├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.gradle ├── codequality ├── HEADER └── checkstyle.xml ├── gradle.properties ├── gradle ├── buildscript.gradle ├── check.gradle ├── convention.gradle ├── license.gradle ├── maven.gradle ├── netflix-oss.gradle ├── release.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle ├── turbine-core ├── build.gradle ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── netflix │ │ │ └── turbine │ │ │ ├── StartTurbine.java │ │ │ ├── Turbine.java │ │ │ ├── aggregator │ │ │ ├── AggregateString.java │ │ │ ├── GroupKey.java │ │ │ ├── InstanceKey.java │ │ │ ├── NumberList.java │ │ │ ├── StreamAggregator.java │ │ │ └── TypeAndNameKey.java │ │ │ ├── discovery │ │ │ ├── StreamAction.java │ │ │ └── StreamDiscovery.java │ │ │ └── internal │ │ │ ├── AggregateStringSerializer.java │ │ │ ├── JsonUtility.java │ │ │ ├── NumberListSerializer.java │ │ │ └── RequestCreator.java │ └── test │ │ ├── java │ │ └── com │ │ │ └── netflix │ │ │ └── turbine │ │ │ ├── Demo.java │ │ │ ├── HttpServerDemo.java │ │ │ ├── HystrixStreamSource.java │ │ │ ├── aggregator │ │ │ ├── AggregateStringTest.java │ │ │ ├── NumberListTest.java │ │ │ └── StreamAggregatorTest.java │ │ │ └── internal │ │ │ └── RequestCreatorTest.java │ │ └── resources │ │ └── com │ │ └── netflix │ │ └── turbine │ │ ├── hystrix-cinematch.stream │ │ ├── hystrix-subscriber.stream │ │ ├── hystrix-subscriber_cinematch_1.stream │ │ ├── hystrix-subscriber_cinematch_2.stream │ │ ├── hystrix.stream │ │ └── unsort └── testfiles │ └── StatsSingleServerMonitorUnitTest.txt └── turbine-ext ├── turbine-discovery-eureka1 ├── build.gradle └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── netflix │ │ │ └── turbine │ │ │ └── discovery │ │ │ └── eureka │ │ │ ├── EurekaInstance.java │ │ │ ├── EurekaInstanceDiscovery.java │ │ │ ├── EurekaStreamDiscovery.java │ │ │ └── StartEurekaTurbine.java │ └── resources │ │ ├── eureka-client.properties │ │ └── log4j.properties │ └── test │ └── java │ └── com │ └── netflix │ └── turbine │ └── discovery │ └── eureka │ └── EurekaInstanceDiscoveryTest.java └── turbine-discovery-eureka2 ├── build.gradle └── src ├── main ├── java │ └── com │ │ └── netflix │ │ └── turbine │ │ └── discovery │ │ └── eureka │ │ ├── EurekaInstance.java │ │ ├── EurekaInstanceDiscovery.java │ │ ├── EurekaStreamDiscovery.java │ │ └── StartEurekaTurbine.java └── resources │ └── log4j.properties └── test └── java └── com └── netflix └── turbine └── discovery └── eureka └── EurekaInstanceDiscoveryTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | 23 | # Logs and databases # 24 | ###################### 25 | *.log 26 | 27 | # OS generated files # 28 | ###################### 29 | .DS_Store* 30 | ehthumbs.db 31 | Icon? 32 | Thumbs.db 33 | 34 | # Editor Files # 35 | ################ 36 | *~ 37 | *.swp 38 | 39 | # Gradle Files # 40 | ################ 41 | .gradle 42 | 43 | # Build output directies 44 | /target 45 | */target 46 | /build 47 | */build 48 | build/ 49 | 50 | # IntelliJ specific files/directories 51 | out 52 | .idea 53 | *.ipr 54 | *.iws 55 | *.iml 56 | atlassian-ide-plugin.xml 57 | 58 | # Eclipse specific files/directories 59 | .classpath 60 | .project 61 | .settings 62 | .metadata 63 | bin/ 64 | 65 | # NetBeans specific files/directories 66 | .nbattrs 67 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: 4 | - oraclejdk8 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2012 Netflix, Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Documentation 2 | 3 | See the [Wiki](https://github.com/Netflix/Turbine/wiki) for full documentation, examples, operational details and other information. 4 | 5 | ## Build Status 6 | 7 | 8 | 9 | ## Bugs and Feedback 10 | 11 | For bugs, questions and discussions please use the [Github Issues](https://github.com/Netflix/Turbine/issues). 12 | 13 | ## Binaries 14 | 15 | Binaries and dependency information for Maven, Ivy, Gradle and others can be found at [http://search.maven.org](http://search.maven.org/#search%7Cga%7C1%7Cnetflix%20turbine). 16 | 17 | ### Library 18 | 19 | Dependencies on the library for embedded use are found on Maven Central. 20 | 21 | Example for Maven: 22 | 23 | ```xml 24 | 25 | com.netflix.turbine 26 | turbine 27 | 2.minor.patch 28 | 29 | ``` 30 | and for Ivy: 31 | 32 | ```xml 33 | 34 | ``` 35 | 36 | ### Executable 37 | 38 | The standalone executable can also be found on Maven Central or in the Github Releases section. 39 | 40 | 41 | ## Build 42 | 43 | * You need Java 8 or later. 44 | 45 | To build: 46 | 47 | ``` 48 | $ git clone git@github.com:Netflix/Turbine.git 49 | $ cd Turbine/ 50 | $ ./gradlew build 51 | ``` 52 | 53 | 54 | ## LICENSE 55 | 56 | Copyright 2014 Netflix, Inc. 57 | 58 | Licensed under the Apache License, Version 2.0 (the "License"); 59 | you may not use this file except in compliance with the License. 60 | You may obtain a copy of the License at 61 | 62 | 63 | 64 | Unless required by applicable law or agreed to in writing, software 65 | distributed under the License is distributed on an "AS IS" BASIS, 66 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 67 | See the License for the specific language governing permissions and 68 | limitations under the License. 69 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Establish version and status 2 | ext.githubProjectName = rootProject.name // Change if github project name is not the same as the root project's name 3 | 4 | buildscript { 5 | repositories { 6 | mavenLocal() 7 | mavenCentral() // maven { url 'http://jcenter.bintray.com' } 8 | jcenter() 9 | } 10 | dependencies { 11 | classpath 'com.github.jengelman.gradle.plugins:shadow:1.1.1' 12 | } 13 | apply from: file('gradle/buildscript.gradle'), to: buildscript 14 | } 15 | 16 | 17 | allprojects { 18 | repositories { 19 | mavenLocal() 20 | mavenCentral() // maven { url 'http://jcenter.bintray.com' } 21 | jcenter() 22 | } 23 | } 24 | 25 | apply from: file('gradle/convention.gradle') 26 | apply from: file('gradle/maven.gradle') 27 | //apply from: file('gradle/check.gradle') 28 | apply from: file('gradle/license.gradle') 29 | apply from: file('gradle/release.gradle') 30 | 31 | tasks.withType(Javadoc).each { task -> 32 | task.enabled = false 33 | } 34 | 35 | subprojects { 36 | 37 | group = "com.netflix.${githubProjectName}" 38 | 39 | tasks.withType(Javadoc).each { task -> 40 | task.enabled = false 41 | } 42 | 43 | sourceSets { 44 | test { 45 | java { 46 | srcDir 'src/main/java' 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /codequality/HEADER: -------------------------------------------------------------------------------- 1 | Copyright ${year} Netflix, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /codequality/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | version=2.0.0-DP.3-SNAPSHOT 2 | -------------------------------------------------------------------------------- /gradle/buildscript.gradle: -------------------------------------------------------------------------------- 1 | // Executed in context of buildscript 2 | repositories { 3 | // Repo in addition to maven central 4 | repositories { maven { url 'http://dl.bintray.com/content/netflixoss/external-gradle-plugins/' } } // For gradle-release 5 | } 6 | dependencies { 7 | classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.1' 8 | classpath 'com.mapvine:gradle-cobertura-plugin:0.1' 9 | classpath 'gradle-release:gradle-release:1.1.5' 10 | classpath 'org.ajoberstar:gradle-git:0.5.0' 11 | } 12 | -------------------------------------------------------------------------------- /gradle/check.gradle: -------------------------------------------------------------------------------- 1 | subprojects { 2 | // Checkstyle 3 | apply plugin: 'checkstyle' 4 | checkstyle { 5 | ignoreFailures = true 6 | configFile = rootProject.file('codequality/checkstyle.xml') 7 | } 8 | 9 | // FindBugs 10 | apply plugin: 'findbugs' 11 | findbugs { 12 | ignoreFailures = true 13 | } 14 | 15 | // PMD 16 | apply plugin: 'pmd' 17 | //tasks.withType(Pmd) { reports.html.enabled true } 18 | 19 | apply plugin: 'cobertura' 20 | cobertura { 21 | sourceDirs = sourceSets.main.java.srcDirs 22 | format = 'html' 23 | includes = ['**/*.java', '**/*.groovy'] 24 | excludes = [] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /gradle/convention.gradle: -------------------------------------------------------------------------------- 1 | // GRADLE-2087 workaround, perform after java plugin 2 | status = project.hasProperty('preferredStatus')?project.preferredStatus:(version.contains('SNAPSHOT')?'snapshot':'release') 3 | 4 | subprojects { project -> 5 | apply plugin: 'java' // Plugin as major conventions 6 | 7 | sourceCompatibility = 1.6 8 | 9 | // Restore status after Java plugin 10 | status = rootProject.status 11 | 12 | task sourcesJar(type: Jar, dependsOn:classes) { 13 | from sourceSets.main.allSource 14 | classifier 'sources' 15 | extension 'jar' 16 | } 17 | 18 | task javadocJar(type: Jar, dependsOn:javadoc) { 19 | from javadoc.destinationDir 20 | classifier 'javadoc' 21 | extension 'jar' 22 | } 23 | 24 | configurations.add('sources') 25 | configurations.add('javadoc') 26 | configurations.archives { 27 | extendsFrom configurations.sources 28 | extendsFrom configurations.javadoc 29 | } 30 | 31 | // When outputing to an Ivy repo, we want to use the proper type field 32 | gradle.taskGraph.whenReady { 33 | def isNotMaven = !it.hasTask(project.uploadMavenCentral) 34 | if (isNotMaven) { 35 | def artifacts = project.configurations.sources.artifacts 36 | def sourceArtifact = artifacts.iterator().next() 37 | sourceArtifact.type = 'sources' 38 | } 39 | } 40 | 41 | artifacts { 42 | sources(sourcesJar) { 43 | // Weird Gradle quirk where type will be used for the extension, but only for sources 44 | type 'jar' 45 | } 46 | javadoc(javadocJar) { 47 | type 'javadoc' 48 | } 49 | } 50 | 51 | configurations { 52 | provided { 53 | description = 'much like compile, but indicates you expect the JDK or a container to provide it. It is only available on the compilation classpath, and is not transitive.' 54 | transitive = true 55 | visible = true 56 | } 57 | } 58 | 59 | project.sourceSets { 60 | main.compileClasspath += project.configurations.provided 61 | main.runtimeClasspath -= project.configurations.provided 62 | test.compileClasspath += project.configurations.provided 63 | test.runtimeClasspath += project.configurations.provided 64 | } 65 | } 66 | 67 | apply plugin: 'github-pages' // Used to create publishGhPages task 68 | 69 | def docTasks = [:] 70 | [Javadoc,ScalaDoc,Groovydoc].each{ Class docClass -> 71 | def allSources = allprojects.tasks*.withType(docClass).flatten()*.source 72 | if (allSources) { 73 | def shortName = docClass.simpleName.toLowerCase() 74 | def docTask = task "aggregate${shortName.capitalize()}"(type: docClass, description: "Aggregate subproject ${shortName}s") { 75 | source = allSources 76 | destinationDir = file("${project.buildDir}/docs/${shortName}") 77 | doFirst { 78 | def classpaths = allprojects.findAll { it.plugins.hasPlugin(JavaPlugin) }.collect { it.sourceSets.main.compileClasspath } 79 | classpath = files(classpaths) 80 | } 81 | } 82 | docTasks[shortName] = docTask 83 | processGhPages.dependsOn(docTask) 84 | } 85 | } 86 | 87 | githubPages { 88 | repoUri = "git@github.com:Netflix/${rootProject.githubProjectName}.git" 89 | pages { 90 | docTasks.each { shortName, docTask -> 91 | from(docTask.outputs.files) { 92 | into "docs/${shortName}" 93 | } 94 | } 95 | } 96 | } 97 | 98 | // Generate wrapper, which is distributed as part of source to alleviate the need of installing gradle 99 | task createWrapper(type: Wrapper) { 100 | gradleVersion = '1.5' 101 | } 102 | -------------------------------------------------------------------------------- /gradle/license.gradle: -------------------------------------------------------------------------------- 1 | // Dependency for plugin was set in buildscript.gradle 2 | 3 | subprojects { 4 | apply plugin: 'license' //nl.javadude.gradle.plugins.license.LicensePlugin 5 | license { 6 | header rootProject.file('codequality/HEADER') 7 | ext.year = Calendar.getInstance().get(Calendar.YEAR) 8 | skipExistingHeaders true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /gradle/maven.gradle: -------------------------------------------------------------------------------- 1 | // Maven side of things 2 | subprojects { 3 | apply plugin: 'maven' // Java plugin has to have been already applied for the conf2scope mappings to work 4 | apply plugin: 'signing' 5 | 6 | signing { 7 | required { gradle.taskGraph.hasTask(uploadMavenCentral) } 8 | sign configurations.archives 9 | } 10 | 11 | /** 12 | * Publishing to Maven Central example provided from http://jedicoder.blogspot.com/2011/11/automated-gradle-project-deployment-to.html 13 | * artifactory will execute uploadArchives to force generation of ivy.xml, and we don't want that to trigger an upload to maven 14 | * central, so using custom upload task. 15 | */ 16 | task uploadMavenCentral(type:Upload, dependsOn: signArchives) { 17 | configuration = configurations.archives 18 | onlyIf { ['release', 'snapshot'].contains(project.status) } 19 | repositories.mavenDeployer { 20 | beforeDeployment { signing.signPom(it) } 21 | 22 | // To test deployment locally, use the following instead of oss.sonatype.org 23 | //repository(url: "file://localhost/${rootProject.rootDir}/repo") 24 | 25 | def sonatypeUsername = rootProject.hasProperty('sonatypeUsername')?rootProject.sonatypeUsername:'' 26 | def sonatypePassword = rootProject.hasProperty('sonatypePassword')?rootProject.sonatypePassword:'' 27 | 28 | repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2') { 29 | authentication(userName: sonatypeUsername, password: sonatypePassword) 30 | } 31 | 32 | snapshotRepository(url: 'https://oss.sonatype.org/content/repositories/snapshots/') { 33 | authentication(userName: sonatypeUsername, password: sonatypePassword) 34 | } 35 | 36 | // Prevent datastamp from being appending to artifacts during deployment 37 | uniqueVersion = false 38 | 39 | // Closure to configure all the POM with extra info, common to all projects 40 | pom.project { 41 | name "${project.name}" 42 | description "${project.name} developed by Netflix" 43 | developers { 44 | developer { 45 | id 'netflixgithub' 46 | name 'Netflix Open Source Development' 47 | email 'talent@netflix.com' 48 | } 49 | } 50 | licenses { 51 | license { 52 | name 'The Apache Software License, Version 2.0' 53 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt' 54 | distribution 'repo' 55 | } 56 | } 57 | url "https://github.com/Netflix/${rootProject.githubProjectName}" 58 | scm { 59 | connection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" 60 | url "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" 61 | developerConnection "scm:git:git@github.com:Netflix/${rootProject.githubProjectName}.git" 62 | } 63 | issueManagement { 64 | system 'github' 65 | url "https://github.com/Netflix/${rootProject.githubProjectName}/issues" 66 | } 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /gradle/netflix-oss.gradle: -------------------------------------------------------------------------------- 1 | apply from: 'http://artifacts.netflix.com/gradle-netflix-local/artifactory.gradle' 2 | -------------------------------------------------------------------------------- /gradle/release.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'release' 2 | 3 | [ uploadIvyLocal: 'uploadLocal', uploadArtifactory: 'artifactoryPublish', buildWithArtifactory: 'build' ].each { key, value -> 4 | // Call out to compile against internal repository 5 | task "${key}"(type: GradleBuild) { 6 | startParameter = project.gradle.startParameter.newInstance() 7 | doFirst { 8 | startParameter.projectProperties = [status: project.status, preferredStatus: project.status] 9 | } 10 | startParameter.addInitScript( file('gradle/netflix-oss.gradle') ) 11 | startParameter.getExcludedTaskNames().add('check') 12 | tasks = [ 'build', value ] 13 | } 14 | } 15 | 16 | // Marker task for following code to key in on 17 | task releaseCandidate(dependsOn: release) 18 | task forceCandidate { 19 | onlyIf { gradle.taskGraph.hasTask(releaseCandidate) } 20 | doFirst { project.status = 'candidate' } 21 | } 22 | task forceRelease { 23 | onlyIf { !gradle.taskGraph.hasTask(releaseCandidate) } 24 | doFirst { project.status = 'release' } 25 | } 26 | release.dependsOn([forceCandidate, forceRelease]) 27 | 28 | task uploadMavenCentral(dependsOn: subprojects.tasks.uploadMavenCentral) 29 | task releaseSnapshot(dependsOn: [uploadArtifactory, uploadMavenCentral]) 30 | 31 | // Ensure our versions look like the project status before publishing 32 | task verifyStatus << { 33 | def hasSnapshot = version.contains('-SNAPSHOT') 34 | if (project.status == 'snapshot' && !hasSnapshot) { 35 | throw new GradleException("Version (${version}) needs -SNAPSHOT if publishing snapshot") 36 | } 37 | } 38 | uploadArtifactory.dependsOn(verifyStatus) 39 | uploadMavenCentral.dependsOn(verifyStatus) 40 | 41 | // Ensure upload happens before taggging, hence upload failures will leave repo in a revertable state 42 | preTagCommit.dependsOn([uploadArtifactory, uploadMavenCentral]) 43 | 44 | 45 | gradle.taskGraph.whenReady { taskGraph -> 46 | def hasRelease = taskGraph.hasTask('commitNewVersion') 47 | def indexOf = { return taskGraph.allTasks.indexOf(it) } 48 | 49 | if (hasRelease) { 50 | assert indexOf(build) < indexOf(unSnapshotVersion), 'build target has to be after unSnapshotVersion' 51 | assert indexOf(uploadMavenCentral) < indexOf(preTagCommit), 'preTagCommit has to be after uploadMavenCentral' 52 | assert indexOf(uploadArtifactory) < indexOf(preTagCommit), 'preTagCommit has to be after uploadArtifactory' 53 | } 54 | } 55 | 56 | // Prevent plugin from asking for a version number interactively 57 | ext.'gradle.release.useAutomaticVersion' = "true" 58 | 59 | release { 60 | git.requireBranch = null 61 | } 62 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Netflix/Turbine/0e924058aa4d1d526310206a51dcf82f65274d58/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Aug 14 16:28:54 PDT 2012 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=http\://services.gradle.org/distributions/gradle-1.10-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 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /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 Windowz 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='turbine' 2 | include 'turbine-core', 'turbine-ext:turbine-discovery-eureka1', 'turbine-ext:turbine-discovery-eureka2' 3 | -------------------------------------------------------------------------------- /turbine-core/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'eclipse' 3 | apply plugin:'application' 4 | apply plugin: 'com.github.johnrengelman.shadow' 5 | apply plugin: 'maven-publish' 6 | 7 | sourceCompatibility = JavaVersion.VERSION_1_8 8 | targetCompatibility = JavaVersion.VERSION_1_8 9 | 10 | dependencies { 11 | compile 'io.reactivex:rxjava:1.0.8' 12 | compile 'com.netflix.rxnetty:rx-netty:0.3.18' 13 | compile 'org.codehaus.jackson:jackson-core-asl:1.9.2' 14 | compile 'org.codehaus.jackson:jackson-mapper-asl:1.9.2' 15 | compile 'net.sf.jopt-simple:jopt-simple:4.8' 16 | compile 'org.slf4j:slf4j-simple:1.7.7' 17 | testCompile 'junit:junit-dep:4.10' 18 | testCompile 'org.json:json:20140107' 19 | } 20 | 21 | mainClassName = "com.netflix.turbine.StartTurbine" 22 | 23 | shadowJar { 24 | baseName = 'turbine-executable' 25 | classifier = '' 26 | } 27 | 28 | publishing { 29 | publications { 30 | shadow(MavenPublication) { 31 | from components.shadow 32 | artifactId = 'turbine-executable' 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /turbine-core/src/main/java/com/netflix/turbine/StartTurbine.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine; 17 | 18 | import java.net.URI; 19 | import java.net.URISyntaxException; 20 | 21 | import joptsimple.OptionParser; 22 | import joptsimple.OptionSet; 23 | 24 | public class StartTurbine { 25 | 26 | public static void main(String[] args) { 27 | OptionParser optionParser = new OptionParser(); 28 | optionParser.accepts("port").withRequiredArg(); 29 | optionParser.accepts("streams").withRequiredArg(); 30 | 31 | OptionSet options = optionParser.parse(args); 32 | int port = -1; 33 | if (!options.has("port")) { 34 | System.err.println("Argument -port required for SSE HTTP server to start on."); 35 | System.exit(-1); 36 | } else { 37 | try { 38 | port = Integer.parseInt(String.valueOf(options.valueOf("port"))); 39 | } catch (NumberFormatException e) { 40 | System.err.println("Value of port must be an integer but was: " + options.valueOf("port")); 41 | } 42 | } 43 | 44 | URI[] streams = null; 45 | if (!options.hasArgument("streams")) { 46 | System.err.println("Argument -streams required with URIs to connect to. Eg. -streams \"http://host1/metrics.stream http://host2/metrics.stream\""); 47 | System.exit(-1); 48 | } else { 49 | String streamsArg = String.valueOf(options.valueOf("streams")); 50 | String[] ss = streamsArg.split(" "); 51 | streams = new URI[ss.length]; 52 | for (int i = 0; i < ss.length; i++) { 53 | try { 54 | streams[i] = new URI(ss[i]); 55 | } catch (URISyntaxException e) { 56 | System.err.println("ERROR: Could not parse stream into URI: " + ss[i]); 57 | System.exit(-1); 58 | } 59 | } 60 | } 61 | 62 | if (streams == null || streams.length == 0) { 63 | System.err.println("There must be at least 1 valid stream URI."); 64 | System.exit(-1); 65 | } 66 | 67 | try { 68 | Turbine.startServerSentEventServer(port, Turbine.aggregateHttpSSE(streams)); 69 | } catch (Throwable e) { 70 | e.printStackTrace(); 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /turbine-core/src/main/java/com/netflix/turbine/Turbine.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine; 17 | 18 | import com.netflix.turbine.aggregator.InstanceKey; 19 | import com.netflix.turbine.aggregator.StreamAggregator; 20 | import com.netflix.turbine.aggregator.TypeAndNameKey; 21 | import com.netflix.turbine.discovery.StreamAction; 22 | import com.netflix.turbine.discovery.StreamAction.ActionType; 23 | import com.netflix.turbine.discovery.StreamDiscovery; 24 | import com.netflix.turbine.internal.JsonUtility; 25 | import io.netty.buffer.ByteBuf; 26 | import io.reactivex.netty.RxNetty; 27 | import io.reactivex.netty.pipeline.PipelineConfigurators; 28 | import io.reactivex.netty.protocol.text.sse.ServerSentEvent; 29 | import org.slf4j.Logger; 30 | import org.slf4j.LoggerFactory; 31 | import rx.Observable; 32 | import rx.observables.GroupedObservable; 33 | 34 | import java.net.URI; 35 | import java.util.Map; 36 | import java.util.concurrent.TimeUnit; 37 | 38 | import static com.netflix.turbine.internal.RequestCreator.createRequest; 39 | 40 | public class Turbine { 41 | 42 | private static final Logger logger = LoggerFactory.getLogger(Turbine.class); 43 | 44 | public static void startServerSentEventServer(int port, Observable>> streams) { 45 | logger.info("Turbine => Starting server on " + port); 46 | 47 | // multicast so multiple concurrent subscribers get the same stream 48 | Observable> publishedStreams = streams 49 | .doOnUnsubscribe(() -> logger.info("Turbine => Unsubscribing aggregation.")) 50 | .doOnSubscribe(() -> logger.info("Turbine => Starting aggregation")) 51 | .flatMap(o -> o).publish().refCount(); 52 | 53 | RxNetty.createHttpServer(port, (request, response) -> { 54 | logger.info("Turbine => SSE Request Received"); 55 | response.getHeaders().setHeader("Content-Type", "text/event-stream"); 56 | return publishedStreams 57 | .doOnUnsubscribe(() -> logger.info("Turbine => Unsubscribing RxNetty server connection")) 58 | .flatMap(data -> { 59 | return response.writeAndFlush(new ServerSentEvent(null, null, JsonUtility.mapToJson(data))); 60 | }); 61 | }, PipelineConfigurators.sseServerConfigurator()).startAndWait(); 62 | } 63 | 64 | public static void startServerSentEventServer(int port, StreamDiscovery discovery) { 65 | startServerSentEventServer(port, aggregateHttpSSE(discovery)); 66 | } 67 | 68 | public static Observable>> aggregateHttpSSE(URI... uris) { 69 | return aggregateHttpSSE(() -> { 70 | return Observable.from(uris).map(uri -> StreamAction.create(ActionType.ADD, uri)).concatWith(Observable.never()); // never() as we don't want to end 71 | }); 72 | } 73 | 74 | /** 75 | * Aggregate multiple HTTP Server-Sent Event streams into one stream with the values summed. 76 | *

77 | * The returned data must be JSON data that contains the following keys: 78 | *

79 | * instanceId => Unique instance representing each stream to be merged, such as the instanceId of the server the stream is from. 80 | * type => The type of data such as HystrixCommand or HystrixThreadPool if aggregating Hystrix metrics. 81 | * name => Name of a group of metrics to be aggregated, such as a HystrixCommand name if aggregating Hystrix metrics. 82 | * 83 | * @param uri 84 | * @return 85 | */ 86 | public static Observable>> aggregateHttpSSE(StreamDiscovery discovery) { 87 | Observable streamActions = discovery.getInstanceList().publish().refCount(); 88 | Observable streamAdds = streamActions.filter(a -> a.getType() == ActionType.ADD); 89 | Observable streamRemoves = streamActions.filter(a -> a.getType() == ActionType.REMOVE); 90 | 91 | Observable>> streamPerInstance = 92 | streamAdds.map(streamAction -> { 93 | URI uri = streamAction.getUri(); 94 | 95 | Observable> io = Observable.defer(() -> { 96 | Observable> flatMap = RxNetty.createHttpClient(uri.getHost(), uri.getPort(), PipelineConfigurators.sseClientConfigurator()) 97 | .submit(createRequest(uri)) 98 | .flatMap(response -> { 99 | if (response.getStatus().code() != 200) { 100 | return Observable.error(new RuntimeException("Failed to connect: " + response.getStatus())); 101 | } 102 | return response.getContent() 103 | .doOnSubscribe(() -> logger.info("Turbine => Aggregate Stream from URI: " + uri.toASCIIString())) 104 | .doOnUnsubscribe(() -> logger.info("Turbine => Unsubscribing Stream: " + uri)) 105 | .takeUntil(streamRemoves.filter(a -> a.getUri().equals(streamAction.getUri()))) // unsubscribe when we receive a remove event 106 | .map(sse -> JsonUtility.jsonToMap(sse.getEventData())); 107 | }); 108 | // eclipse is having issues with type inference so breaking up 109 | return flatMap.retryWhen(attempts -> { 110 | return attempts.flatMap(e -> { 111 | return Observable.timer(1, TimeUnit.SECONDS) 112 | .doOnEach(n -> logger.info("Turbine => Retrying connection to: " + uri)); 113 | }); 114 | }); 115 | 116 | }); 117 | 118 | return GroupedObservable.from(InstanceKey.create(uri.toASCIIString()), io); 119 | }); 120 | 121 | return StreamAggregator.aggregateGroupedStreams(streamPerInstance); 122 | } 123 | 124 | /** 125 | * Aggregate multiple HTTP URIs 126 | * 127 | * @param uri 128 | * @return 129 | */ 130 | // public static Observable>> aggregateHttp(java.net.URI... uri) { 131 | // 132 | // } 133 | } 134 | -------------------------------------------------------------------------------- /turbine-core/src/main/java/com/netflix/turbine/aggregator/AggregateString.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.aggregator; 17 | 18 | import java.util.Collections; 19 | import java.util.HashSet; 20 | import java.util.Map; 21 | import java.util.Map.Entry; 22 | import java.util.Set; 23 | import java.util.TreeMap; 24 | 25 | import org.codehaus.jackson.map.annotate.JsonSerialize; 26 | 27 | import com.netflix.turbine.internal.AggregateStringSerializer; 28 | import com.netflix.turbine.internal.JsonUtility; 29 | 30 | /** 31 | * Represents the different sets of string values that have been received for a given key 32 | * and the respective value counts. 33 | * 34 | * For example, if 2 "false" and 1 "true" were set on this the output would be: {"false":2,"true":1} 35 | * 36 | * This class is optimized for the fact that almost all the time the value is expected to be the same and only rarely have more than 1 value. 37 | */ 38 | @JsonSerialize(using = AggregateStringSerializer.class) 39 | public class AggregateString { 40 | 41 | private final Map values; 42 | private final Set instances; 43 | 44 | private AggregateString(Map values, Set instances) { 45 | this.values = values; 46 | this.instances = instances; 47 | } 48 | 49 | private static AggregateString EMPTY = new AggregateString(Collections.emptyMap(), Collections.emptySet()); 50 | 51 | public static AggregateString create() { 52 | return EMPTY; 53 | } 54 | 55 | public static AggregateString create(String value, InstanceKey instanceKey) { 56 | if (instanceKey == null) { 57 | throw new NullPointerException("AggregateString can not have null InstanceKey. Value -> " + value); 58 | } 59 | return new AggregateString(Collections.singletonMap(value, 1), Collections.singleton(instanceKey)); 60 | } 61 | 62 | /** 63 | * Update a value for an instance. 64 | *

65 | * To completely remove a value and its instance, pass in null for newValue 66 | * 67 | * @param oldValue 68 | * @param newValue 69 | * @param instanceKey 70 | * @return 71 | */ 72 | public AggregateString update(String oldValue, String newValue, InstanceKey instanceKey) { 73 | if (instanceKey == null) { 74 | throw new NullPointerException("AggregateString can not have null InstanceKey. Value -> " + newValue); 75 | } 76 | boolean containsInstance = instances.contains(instanceKey); 77 | boolean valuesEqual = valuesEqual(oldValue, newValue); 78 | if (containsInstance && valuesEqual) { 79 | // no change 80 | return this; 81 | } else { 82 | Set _instances; 83 | if (containsInstance && newValue != null) { 84 | _instances = instances; // pass thru 85 | } else if (containsInstance && newValue == null) { 86 | _instances = new HashSet(instances); // clone 87 | _instances.remove(instanceKey); 88 | } else { 89 | _instances = new HashSet(instances); // clone 90 | _instances.add(instanceKey); 91 | } 92 | 93 | Map _values; 94 | if (valuesEqual) { 95 | _values = values; // pass thru 96 | } else { 97 | _values = new TreeMap(values); // clone 98 | if (oldValue != null) { 99 | _values.computeIfPresent(oldValue, (key, old) -> { 100 | if (old == 1) { 101 | return null; // remove 102 | } else { 103 | return old - 1; 104 | } 105 | }); 106 | } 107 | if (newValue != null) { 108 | _values.merge(newValue, 1, (e, v) -> { 109 | if (e == null) { 110 | return v; 111 | } else { 112 | return e + v; 113 | } 114 | }); 115 | } 116 | } 117 | 118 | return new AggregateString(_values, _instances); 119 | } 120 | } 121 | 122 | private boolean valuesEqual(String newValue, String oldValue) { 123 | if (newValue == oldValue) 124 | return true; 125 | if (newValue == null) { 126 | if (oldValue != null) 127 | return false; 128 | } else if (!newValue.equals(oldValue)) { 129 | return false; 130 | } 131 | return true; 132 | } 133 | 134 | public Map values() { 135 | return Collections.unmodifiableMap(values); 136 | } 137 | 138 | public Set instances() { 139 | return Collections.unmodifiableSet(instances); 140 | } 141 | 142 | public String toJson() { 143 | return JsonUtility.mapToJson(values); 144 | } 145 | 146 | @Override 147 | public String toString() { 148 | return getClass().getSimpleName() + " => " + toJson(); 149 | } 150 | 151 | } -------------------------------------------------------------------------------- /turbine-core/src/main/java/com/netflix/turbine/aggregator/GroupKey.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.aggregator; 17 | 18 | public interface GroupKey { 19 | public String getKey(); 20 | } -------------------------------------------------------------------------------- /turbine-core/src/main/java/com/netflix/turbine/aggregator/InstanceKey.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.aggregator; 17 | 18 | public final class InstanceKey implements GroupKey { 19 | final String key; 20 | 21 | public static InstanceKey create(Number id) { 22 | return new InstanceKey(id.toString()); 23 | } 24 | 25 | public static InstanceKey create(String id) { 26 | return new InstanceKey(id); 27 | } 28 | 29 | private InstanceKey(String key) { 30 | if (key == null) { 31 | throw new NullPointerException("InstanceKey can not have null key"); 32 | } 33 | this.key = key; 34 | } 35 | 36 | public String getKey() { 37 | return key; 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return "InstanceKey=>" + key; 43 | } 44 | 45 | @Override 46 | public int hashCode() { 47 | final int prime = 31; 48 | int result = 1; 49 | result = prime * result + ((key == null) ? 0 : key.hashCode()); 50 | return result; 51 | } 52 | 53 | @Override 54 | public boolean equals(Object obj) { 55 | if (this == obj) 56 | return true; 57 | if (obj == null) 58 | return false; 59 | if (getClass() != obj.getClass()) 60 | return false; 61 | InstanceKey other = (InstanceKey) obj; 62 | if (key == null) { 63 | if (other.key != null) 64 | return false; 65 | } else if (!key.equals(other.key)) 66 | return false; 67 | return true; 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /turbine-core/src/main/java/com/netflix/turbine/aggregator/NumberList.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.aggregator; 17 | 18 | import java.util.Collections; 19 | import java.util.LinkedHashMap; 20 | import java.util.Map; 21 | import java.util.Map.Entry; 22 | import java.util.Set; 23 | 24 | import org.codehaus.jackson.map.annotate.JsonSerialize; 25 | 26 | import com.netflix.turbine.internal.JsonUtility; 27 | import com.netflix.turbine.internal.NumberListSerializer; 28 | 29 | @JsonSerialize(using = NumberListSerializer.class) 30 | public class NumberList { 31 | 32 | public static NumberList create(Map numbers) { 33 | LinkedHashMap values = new LinkedHashMap(numbers.size()); 34 | for (Entry k : numbers.entrySet()) { 35 | Object v = k.getValue(); 36 | if (v instanceof Number) { 37 | values.put(k.getKey(), ((Number) v).longValue()); 38 | } else { 39 | values.put(k.getKey(), Long.valueOf(String.valueOf(v))); 40 | } 41 | } 42 | 43 | return new NumberList(values); 44 | } 45 | 46 | // unchecked but we know we can go from Map to Map 47 | @SuppressWarnings("unchecked") 48 | public static NumberList delta(Map currentMap, NumberList previousMap) { 49 | return delta(currentMap, (Map)previousMap.numbers); 50 | } 51 | 52 | // unchecked but we know we can go from Map to Map 53 | @SuppressWarnings("unchecked") 54 | public static NumberList delta(NumberList currentMap, NumberList previousMap) { 55 | return delta((Map)currentMap.numbers, (Map)previousMap.numbers); 56 | } 57 | 58 | /** 59 | * This assumes both maps contain the same keys. If they don't then keys will be lost. 60 | * 61 | * @param currentMap 62 | * @param previousMap 63 | * @return 64 | */ 65 | public static NumberList delta(Map currentMap, Map previousMap) { 66 | LinkedHashMap values = new LinkedHashMap(currentMap.size()); 67 | if(currentMap.size() != previousMap.size()) { 68 | throw new IllegalArgumentException("Maps must have the same keys"); 69 | } 70 | for (Entry k : currentMap.entrySet()) { 71 | Object v = k.getValue(); 72 | Number current = getNumber(v); 73 | Object p = previousMap.get(k.getKey()); 74 | Number previous = null; 75 | if (p == null) { 76 | previous = 0; 77 | } else { 78 | previous = getNumber(p); 79 | } 80 | 81 | long d = (current.longValue() - previous.longValue()); 82 | values.put(k.getKey(), d); 83 | } 84 | 85 | return new NumberList(values); 86 | } 87 | 88 | /** 89 | * Return a NubmerList with the inverse of the given values. 90 | */ 91 | public static NumberList deltaInverse(Map map) { 92 | LinkedHashMap values = new LinkedHashMap(map.size()); 93 | for (Entry k : map.entrySet()) { 94 | Object v = k.getValue(); 95 | Number current = getNumber(v); 96 | long d = -current.longValue(); 97 | values.put(k.getKey(), d); 98 | } 99 | 100 | return new NumberList(values); 101 | } 102 | 103 | public NumberList sum(NumberList delta) { 104 | LinkedHashMap values = new LinkedHashMap(numbers.size()); 105 | for (Entry k : numbers.entrySet()) { 106 | Long p = k.getValue(); 107 | Long d = delta.get(k.getKey()); 108 | Long previous = null; 109 | if (p == null) { 110 | previous = 0L; 111 | } else { 112 | previous = p; 113 | } 114 | 115 | // if we're missing the value in the delta we negate it 116 | if (d == null) { 117 | d = -previous; 118 | } 119 | long sum = d + previous; 120 | values.put(k.getKey(), sum); 121 | } 122 | 123 | return new NumberList(values); 124 | } 125 | 126 | private static Number getNumber(Object v) { 127 | Number n = null; 128 | if (v instanceof Number) { 129 | n = ((Number) v).longValue(); 130 | } else { 131 | n = Long.valueOf(String.valueOf(v)); 132 | } 133 | return n; 134 | } 135 | 136 | public static NumberList empty() { 137 | return create(Collections.emptyMap()); 138 | } 139 | 140 | private final Map numbers; 141 | 142 | private NumberList(Map numbers) { 143 | this.numbers = numbers; 144 | } 145 | 146 | public Map getMap() { 147 | return numbers; 148 | } 149 | 150 | public Long get(String key) { 151 | return numbers.get(key); 152 | } 153 | 154 | public Set> getEntries() { 155 | return numbers.entrySet(); 156 | } 157 | 158 | @Override 159 | public String toString() { 160 | return getClass().getSimpleName() + " => " + numbers.entrySet(); 161 | } 162 | 163 | public String toJson() { 164 | return JsonUtility.mapToJson(numbers); 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /turbine-core/src/main/java/com/netflix/turbine/aggregator/StreamAggregator.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.aggregator; 17 | 18 | import java.util.Collections; 19 | import java.util.HashMap; 20 | import java.util.HashSet; 21 | import java.util.LinkedHashMap; 22 | import java.util.List; 23 | import java.util.Map; 24 | 25 | import rx.Observable; 26 | import rx.observables.GroupedObservable; 27 | 28 | public class StreamAggregator { 29 | 30 | public static Observable>> aggregateGroupedStreams(Observable>> stream) { 31 | return aggregateUsingFlattenedGroupBy(stream); 32 | } 33 | 34 | private StreamAggregator() { 35 | 36 | } 37 | 38 | /** 39 | * Flatten the stream and then do nested groupBy. This matches the mental model and is simple but 40 | * serializes the stream to a single thread which is bad for performance. 41 | * 42 | * TODO: Add a version like this but with a ParallelObservable 43 | * 44 | * @param stream 45 | * @return 46 | */ 47 | @SuppressWarnings("unused") 48 | private static Observable>> aggregateUsingFlattenedGroupBy(Observable>> stream) { 49 | Observable> allData = stream.flatMap(instanceStream -> { 50 | return instanceStream 51 | .map((Map event) -> { 52 | event.put("InstanceKey", instanceStream.getKey()); 53 | event.put("TypeAndName", TypeAndNameKey.from(String.valueOf(event.get("type")), String.valueOf(event.get("name")))); 54 | return event; 55 | }) 56 | .compose(is -> { 57 | return tombstone(is, instanceStream.getKey()); 58 | }); 59 | }); 60 | 61 | Observable>> byCommand = allData 62 | .groupBy((Map event) -> { 63 | return (TypeAndNameKey) event.get("TypeAndName"); 64 | }); 65 | 66 | return byCommand 67 | .map(commandGroup -> { 68 | Observable> sumOfDeltasForAllInstancesForCommand = commandGroup 69 | .groupBy((Map json) -> { 70 | return json.get("InstanceKey"); 71 | }).flatMap(instanceGroup -> { 72 | // calculate and output deltas for each instance stream per command 73 | return instanceGroup 74 | .takeUntil(d -> d.containsKey("tombstone")) 75 | .startWith(Collections. emptyMap()) 76 | .map(data -> { 77 | if (data.containsKey("tombstone")) { 78 | return Collections. emptyMap(); 79 | } else { 80 | return data; 81 | } 82 | }) 83 | .buffer(2, 1) 84 | .filter(list -> list.size() == 2) 85 | .map(StreamAggregator::previousAndCurrentToDelta) 86 | .filter(data -> data != null && !data.isEmpty()); 87 | }) 88 | // we now have all instance deltas merged into a single stream per command 89 | // and sum them into a single stream of aggregate values 90 | .scan(new HashMap(), StreamAggregator::sumOfDelta) 91 | .skip(1); 92 | 93 | // we artificially wrap in a GroupedObservable that communicates the CommandKey this stream represents 94 | return GroupedObservable.from(commandGroup.getKey(), sumOfDeltasForAllInstancesForCommand); 95 | }); 96 | } 97 | 98 | /** 99 | * Append tombstone events to each instanceStream when they terminate so that the last values from the stream 100 | * will be removed from all aggregates down stream. 101 | * 102 | * @param instanceStream 103 | * @param instanceKey 104 | */ 105 | private static Observable> tombstone(Observable> instanceStream, InstanceKey instanceKey) { 106 | return instanceStream.publish(is -> { 107 | Observable> tombstone = is 108 | // collect all unique "TypeAndName" keys 109 | .collect(() -> new HashSet(), (listOfTypeAndName, event) -> { 110 | listOfTypeAndName.add((TypeAndNameKey) event.get("TypeAndName")); 111 | }) 112 | // when instanceStream completes, emit a "tombstone" for each "TypeAndName" in the HashSet collected above 113 | .flatMap(listOfTypeAndName -> { 114 | return Observable.from(listOfTypeAndName) 115 | .map(typeAndName -> { 116 | Map tombstoneForTypeAndName = new LinkedHashMap<>(); 117 | tombstoneForTypeAndName.put("tombstone", "true"); 118 | tombstoneForTypeAndName.put("InstanceKey", instanceKey); 119 | tombstoneForTypeAndName.put("TypeAndName", typeAndName); 120 | tombstoneForTypeAndName.put("name", typeAndName.getName()); 121 | tombstoneForTypeAndName.put("type", typeAndName.getType()); 122 | return tombstoneForTypeAndName; 123 | }); 124 | }); 125 | 126 | // concat the tombstone events to the end of the original stream 127 | return is.mergeWith(tombstone); 128 | }); 129 | } 130 | 131 | /** 132 | * This expects to receive a list of 2 items. If it is a different size it will throw an exception. 133 | * 134 | * @param data 135 | * @return 136 | */ 137 | /* package for unit tests */static final Map previousAndCurrentToDelta(List> data) { 138 | if (data.size() == 2) { 139 | Map previous = data.get(0); 140 | Map current = data.get(1); 141 | return previousAndCurrentToDelta(previous, current); 142 | } else { 143 | throw new IllegalArgumentException("Must be list of 2 items"); 144 | } 145 | 146 | } 147 | 148 | @SuppressWarnings("unchecked") 149 | /* package for unit tests */static final Map previousAndCurrentToDelta(Map previous, Map current) { 150 | if (previous.isEmpty()) { 151 | // the first time through it is empty so we'll emit the current to start 152 | Map seed = new LinkedHashMap(); 153 | initMapWithIdentifiers(current, seed); 154 | 155 | for (String key : current.keySet()) { 156 | if (isIdentifierKey(key)) { 157 | continue; 158 | } 159 | Object currentValue = current.get(key); 160 | if (currentValue instanceof Number && !key.startsWith("propertyValue_")) { 161 | // convert all numbers to Long so they are consistent 162 | seed.put(key, ((Number) currentValue).longValue()); 163 | } else if (currentValue instanceof Map) { 164 | // NOTE: we are expecting maps to only contain key/value pairs with values as numbers 165 | seed.put(key, NumberList.create((Map) currentValue)); 166 | } else { 167 | seed.put(key, new String[] { String.valueOf(currentValue) }); 168 | } 169 | } 170 | return seed; 171 | } else if (current.isEmpty() || containsOnlyIdentifiers(current)) { 172 | // we are terminating so the delta we emit needs to remove everything 173 | Map delta = new LinkedHashMap(); 174 | initMapWithIdentifiers(previous, delta); 175 | 176 | for (String key : previous.keySet()) { 177 | if (isIdentifierKey(key)) { 178 | continue; 179 | } 180 | Object previousValue = previous.get(key); 181 | if (previousValue instanceof Number && !key.startsWith("propertyValue_")) { 182 | Number previousValueAsNumber = (Number) previousValue; 183 | long d = -previousValueAsNumber.longValue(); 184 | delta.put(key, d); 185 | } else if (previousValue instanceof Map) { 186 | delta.put(key, NumberList.deltaInverse((Map) previousValue)); 187 | } else { 188 | delta.put(key, new String[] { String.valueOf(previousValue), null }); 189 | } 190 | } 191 | return delta; 192 | } else { 193 | // we have previous and current so calculate delta 194 | Map delta = new LinkedHashMap(); 195 | initMapWithIdentifiers(current, delta); 196 | 197 | for (String key : current.keySet()) { 198 | if (isIdentifierKey(key)) { 199 | continue; 200 | } 201 | Object previousValue = previous.get(key); 202 | Object currentValue = current.get(key); 203 | if (currentValue instanceof Number && !key.startsWith("propertyValue_")) { 204 | if (previousValue == null) { 205 | previousValue = 0; 206 | } 207 | Number previousValueAsNumber = (Number) previousValue; 208 | if (currentValue != null) { 209 | Number currentValueAsNumber = (Number) currentValue; 210 | long d = (currentValueAsNumber.longValue() - previousValueAsNumber.longValue()); 211 | delta.put(key, d); 212 | } 213 | } else if (currentValue instanceof Map) { 214 | if (previousValue == null) { 215 | delta.put(key, NumberList.create((Map) currentValue)); 216 | } else { 217 | delta.put(key, NumberList.delta((Map) currentValue, (Map) previousValue)); 218 | } 219 | } else { 220 | delta.put(key, new String[] { String.valueOf(previousValue), String.valueOf(currentValue) }); 221 | 222 | } 223 | } 224 | return delta; 225 | } 226 | } 227 | 228 | private static boolean isIdentifierKey(String key) { 229 | return key.equals("InstanceKey") || key.equals("TypeAndName") || key.equals("instanceId") || key.equals("currentTime") || key.equals("name") || key.equals("type"); 230 | } 231 | 232 | private static boolean containsOnlyIdentifiers(Map m) { 233 | for (String k : m.keySet()) { 234 | if (!isIdentifierKey(k)) { 235 | return false; 236 | } 237 | } 238 | return true; 239 | } 240 | 241 | private static void initMapWithIdentifiers(Map source, Map toInit) { 242 | toInit.put("InstanceKey", source.get("InstanceKey")); // we don't aggregate this, just pass it through 243 | toInit.put("TypeAndName", source.get("TypeAndName")); // we don't aggregate this, just pass it through 244 | toInit.put("instanceId", source.get("instanceId")); // we don't aggregate this, just pass it through 245 | toInit.put("name", source.get("name")); // we don't aggregate this, just pass it through 246 | toInit.put("type", source.get("type")); // we don't aggregate this, just pass it through 247 | } 248 | 249 | /* package for unit tests */static Map sumOfDelta(Map state, Map delta) { 250 | InstanceKey instanceId = (InstanceKey) delta.get("InstanceKey"); 251 | if (instanceId == null) { 252 | throw new RuntimeException("InstanceKey can not be null"); 253 | } 254 | for (String key : delta.keySet()) { 255 | Object existing = state.get(key); 256 | Object current = delta.get(key); 257 | if (current instanceof Number) { 258 | if (existing == null) { 259 | existing = 0; 260 | } 261 | Number v = (Number) existing; 262 | Number d = (Number) delta.get(key); 263 | state.put(key, v.longValue() + d.longValue()); 264 | } else if (current instanceof NumberList) { 265 | if (existing == null) { 266 | state.put(key, current); 267 | } else { 268 | state.put(key, ((NumberList) existing).sum((NumberList) current)); 269 | } 270 | } else { 271 | Object o = delta.get(key); 272 | if (o instanceof String[]) { 273 | String[] vs = (String[]) o; 274 | if (vs.length == 1) { 275 | Object previousAggregateString = state.get(key); 276 | if (previousAggregateString instanceof AggregateString) { 277 | state.put(key, ((AggregateString) previousAggregateString).update(null, vs[0], instanceId)); 278 | } else { 279 | state.put(key, AggregateString.create(vs[0], instanceId)); 280 | } 281 | } else { 282 | // it should always be AggregateString here since that's all we add above 283 | AggregateString pas = (AggregateString) state.get(key); 284 | state.put(key, pas.update(vs[0], vs[1], instanceId)); 285 | } 286 | } else { 287 | state.put(key, String.valueOf(o)); 288 | } 289 | 290 | } 291 | } 292 | return state; 293 | } 294 | 295 | } 296 | -------------------------------------------------------------------------------- /turbine-core/src/main/java/com/netflix/turbine/aggregator/TypeAndNameKey.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.aggregator; 17 | 18 | import java.util.concurrent.ConcurrentHashMap; 19 | 20 | public final class TypeAndNameKey implements GroupKey { 21 | private final String key; 22 | private final String type; 23 | private final String name; 24 | 25 | private static ConcurrentHashMap internedKeys = new ConcurrentHashMap<>(); 26 | 27 | public static TypeAndNameKey from(String type, String name) { 28 | // I wish there was a way to do compound keys without creating new strings 29 | return internedKeys.computeIfAbsent(type + "_" + name, k -> new TypeAndNameKey(k, type, name)); 30 | } 31 | 32 | private TypeAndNameKey(String key, String type, String name) { 33 | this.key = key; 34 | this.type = type; 35 | this.name = name; 36 | } 37 | 38 | public String getKey() { 39 | return key; 40 | } 41 | 42 | public String getType() { 43 | return type; 44 | } 45 | 46 | public String getName() { 47 | return name; 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | return "TypeAndName=>" + key; 53 | } 54 | 55 | @Override 56 | public int hashCode() { 57 | final int prime = 31; 58 | int result = 1; 59 | result = prime * result + ((key == null) ? 0 : key.hashCode()); 60 | return result; 61 | } 62 | 63 | @Override 64 | public boolean equals(Object obj) { 65 | if (this == obj) 66 | return true; 67 | if (obj == null) 68 | return false; 69 | if (getClass() != obj.getClass()) 70 | return false; 71 | TypeAndNameKey other = (TypeAndNameKey) obj; 72 | if (key == null) { 73 | if (other.key != null) 74 | return false; 75 | } else if (!key.equals(other.key)) 76 | return false; 77 | return true; 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /turbine-core/src/main/java/com/netflix/turbine/discovery/StreamAction.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.discovery; 17 | 18 | import java.net.URI; 19 | 20 | public class StreamAction { 21 | 22 | public static enum ActionType { 23 | ADD, REMOVE 24 | } 25 | 26 | private final ActionType type; 27 | private final URI uri; 28 | 29 | private StreamAction(ActionType type, URI uri) { 30 | this.type = type; 31 | this.uri = uri; 32 | } 33 | 34 | public static StreamAction create(ActionType type, URI uri) { 35 | return new StreamAction(type, uri); 36 | } 37 | 38 | public ActionType getType() { 39 | return type; 40 | } 41 | 42 | public URI getUri() { 43 | return uri; 44 | } 45 | 46 | @Override 47 | public int hashCode() { 48 | final int prime = 31; 49 | int result = 1; 50 | result = prime * result + ((type == null) ? 0 : type.hashCode()); 51 | result = prime * result + ((uri == null) ? 0 : uri.hashCode()); 52 | return result; 53 | } 54 | 55 | @Override 56 | public boolean equals(Object obj) { 57 | if (this == obj) 58 | return true; 59 | if (obj == null) 60 | return false; 61 | if (getClass() != obj.getClass()) 62 | return false; 63 | StreamAction other = (StreamAction) obj; 64 | if (type != other.type) 65 | return false; 66 | if (uri == null) { 67 | if (other.uri != null) 68 | return false; 69 | } else if (!uri.equals(other.uri)) 70 | return false; 71 | return true; 72 | } 73 | 74 | @Override 75 | public String toString() { 76 | return "StreamAction [type=" + type + ", uri=" + uri + "]"; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /turbine-core/src/main/java/com/netflix/turbine/discovery/StreamDiscovery.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2012 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.discovery; 17 | 18 | import rx.Observable; 19 | 20 | /** 21 | * Interface used by Turbine to get Instances it will connect to. 22 | */ 23 | public interface StreamDiscovery { 24 | 25 | /** 26 | * Observable of ADD/REMOVE events for Stream URIs 27 | * 28 | * @return Observable 29 | */ 30 | public Observable getInstanceList(); 31 | } -------------------------------------------------------------------------------- /turbine-core/src/main/java/com/netflix/turbine/internal/AggregateStringSerializer.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.internal; 17 | 18 | import java.io.IOException; 19 | import java.util.Map; 20 | 21 | import org.codehaus.jackson.JsonGenerator; 22 | import org.codehaus.jackson.JsonProcessingException; 23 | import org.codehaus.jackson.map.JsonSerializer; 24 | import org.codehaus.jackson.map.SerializerProvider; 25 | 26 | import com.netflix.turbine.aggregator.AggregateString; 27 | 28 | public class AggregateStringSerializer extends JsonSerializer { 29 | 30 | @Override 31 | public void serialize(AggregateString as, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException { 32 | Map values = as.values(); 33 | if (values.size() == 1) { 34 | String k = values.entrySet().iterator().next().getKey(); 35 | if ("false".equals(k)) { 36 | jgen.writeBoolean(false); 37 | } else if ("true".equals(k)) { 38 | jgen.writeBoolean(true); 39 | } else { 40 | jgen.writeString(k); 41 | } 42 | } else { 43 | jgen.writeObject(values); 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /turbine-core/src/main/java/com/netflix/turbine/internal/JsonUtility.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.internal; 17 | 18 | import java.io.IOException; 19 | import java.util.Map; 20 | 21 | import org.codehaus.jackson.map.ObjectMapper; 22 | import org.codehaus.jackson.map.ObjectReader; 23 | import org.codehaus.jackson.map.ObjectWriter; 24 | 25 | public class JsonUtility { 26 | 27 | private static final JsonUtility INSTANCE = new JsonUtility(); 28 | 29 | private JsonUtility() { 30 | 31 | } 32 | 33 | public static Map jsonToMap(String jsonString) { 34 | return INSTANCE._jsonToMap(jsonString); 35 | } 36 | 37 | public static String mapToJson(Map map) { 38 | return INSTANCE._mapToJson(map); 39 | } 40 | 41 | private final ObjectMapper objectMapper = new ObjectMapper(); 42 | private final ObjectReader objectReader = objectMapper.reader(Map.class); 43 | 44 | private Map _jsonToMap(String jsonString) { 45 | try { 46 | return objectReader.readValue(jsonString); 47 | } catch (IOException e) { 48 | throw new RuntimeException("Unable to parse JSON", e); 49 | } 50 | } 51 | 52 | private final ObjectWriter objectWriter = objectMapper.writerWithType(Map.class); 53 | 54 | private String _mapToJson(Map map) { 55 | try { 56 | return objectWriter.writeValueAsString(map); 57 | } catch (IOException e) { 58 | throw new RuntimeException("Unable to write JSON", e); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /turbine-core/src/main/java/com/netflix/turbine/internal/NumberListSerializer.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.internal; 17 | 18 | import java.io.IOException; 19 | 20 | import org.codehaus.jackson.JsonGenerator; 21 | import org.codehaus.jackson.JsonProcessingException; 22 | import org.codehaus.jackson.map.JsonSerializer; 23 | import org.codehaus.jackson.map.SerializerProvider; 24 | 25 | import com.netflix.turbine.aggregator.NumberList; 26 | 27 | /** 28 | * Need to serialize like this => "latencyExecute":{"0":0,"25":0,"50":2,"75":12,"90":16,"95":22,"99":47,"99.5":363,"100":390} 29 | * 30 | */ 31 | public class NumberListSerializer extends JsonSerializer { 32 | 33 | @Override 34 | public void serialize(NumberList nl, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException { 35 | jgen.writeObject(nl.getMap()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /turbine-core/src/main/java/com/netflix/turbine/internal/RequestCreator.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.internal; 17 | 18 | import io.netty.buffer.ByteBuf; 19 | import io.reactivex.netty.protocol.http.client.HttpClientRequest; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | 23 | import java.net.URI; 24 | import java.net.URISyntaxException; 25 | import java.util.Base64; 26 | 27 | import static java.nio.charset.StandardCharsets.UTF_8; 28 | 29 | public class RequestCreator { 30 | 31 | private static final Logger logger = LoggerFactory.getLogger(RequestCreator.class); 32 | 33 | /** 34 | * Creates a {@link HttpClientRequest} for the supplied URI. 35 | * If user info is defined in the URI it'll be applied as basic authentication. 36 | * 37 | * @param uri The URI 38 | * @return The generated {@link HttpClientRequest} with basic auth (if applicable) 39 | */ 40 | public static HttpClientRequest createRequest(URI uri) { 41 | String uriToUse = stripUserInfoFromUriIfDefined(uri).toASCIIString(); 42 | HttpClientRequest request = HttpClientRequest.createGet(uriToUse); 43 | 44 | if (uri.getUserInfo() != null) { 45 | logger.debug("Adding basic authentication header for URI {}", uriToUse); 46 | String basicAuth = "Basic " + Base64.getEncoder().encodeToString(uri.getUserInfo().getBytes(UTF_8)); 47 | request.withHeader("Authorization", basicAuth); 48 | } 49 | 50 | return request; 51 | } 52 | 53 | private static URI stripUserInfoFromUriIfDefined(URI uri) { 54 | final URI uriToUse; 55 | if (uri.getUserInfo() != null) { 56 | try { 57 | uriToUse = new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment()); 58 | } catch (URISyntaxException e) { 59 | throw new RuntimeException(e); 60 | } 61 | } else { 62 | uriToUse = uri; 63 | } 64 | return uriToUse; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /turbine-core/src/test/java/com/netflix/turbine/Demo.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine; 17 | 18 | import java.util.Map; 19 | import java.util.concurrent.TimeUnit; 20 | 21 | import rx.Observable; 22 | import rx.observables.GroupedObservable; 23 | 24 | import com.netflix.turbine.aggregator.InstanceKey; 25 | import com.netflix.turbine.aggregator.StreamAggregator; 26 | import com.netflix.turbine.internal.JsonUtility; 27 | 28 | public class Demo { 29 | 30 | public static void main(String[] args) { 31 | getStream().toBlocking().forEach(map -> { 32 | System.out.println("data => " + JsonUtility.mapToJson(map)); 33 | }); 34 | 35 | } 36 | 37 | public static Observable> getStream() { 38 | GroupedObservable> hystrixStreamA = HystrixStreamSource.getHystrixStreamFromFile(HystrixStreamSource.STREAM_SUBSCRIBER_CINEMATCH_1, 12345, 500); 39 | GroupedObservable> hystrixStreamB = HystrixStreamSource.getHystrixStreamFromFile(HystrixStreamSource.STREAM_SUBSCRIBER_CINEMATCH_1, 23456, 500); 40 | GroupedObservable> hystrixStreamC = HystrixStreamSource.getHystrixStreamFromFile(HystrixStreamSource.STREAM_SUBSCRIBER_CINEMATCH_1, 34567, 500); 41 | GroupedObservable> hystrixStreamD = HystrixStreamSource.getHystrixStreamFromFile(HystrixStreamSource.STREAM_SUBSCRIBER_CINEMATCH_1, 45678, 500); 42 | 43 | Observable>> fullStream = Observable.just(hystrixStreamA, hystrixStreamB, hystrixStreamC, hystrixStreamD); 44 | return StreamAggregator.aggregateGroupedStreams(fullStream).flatMap(commandGroup -> { 45 | return commandGroup 46 | .throttleFirst(1000, TimeUnit.MILLISECONDS); 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /turbine-core/src/test/java/com/netflix/turbine/HttpServerDemo.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine; 17 | 18 | import com.netflix.turbine.internal.JsonUtility; 19 | 20 | import io.reactivex.netty.RxNetty; 21 | import io.reactivex.netty.pipeline.PipelineConfigurators; 22 | import io.reactivex.netty.protocol.text.sse.ServerSentEvent; 23 | 24 | public class HttpServerDemo { 25 | 26 | public static void main(String args[]) { 27 | RxNetty.createHttpServer(8080, (request, response) -> { 28 | return Demo.getStream().flatMap(data -> { 29 | response.getHeaders().set("Content-Type", "text/event-stream"); 30 | return response.writeAndFlush(new ServerSentEvent("1", "data", JsonUtility.mapToJson(data))); 31 | }); 32 | }, PipelineConfigurators.sseServerConfigurator()).startAndWait(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /turbine-core/src/test/java/com/netflix/turbine/HystrixStreamSource.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine; 17 | 18 | 19 | import java.io.BufferedReader; 20 | import java.io.InputStream; 21 | import java.io.InputStreamReader; 22 | import java.util.Map; 23 | 24 | import com.netflix.turbine.aggregator.InstanceKey; 25 | import com.netflix.turbine.internal.JsonUtility; 26 | 27 | import rx.Observable; 28 | import rx.Observable.OnSubscribe; 29 | import rx.Subscriber; 30 | import rx.functions.Action1; 31 | import rx.observables.GroupedObservable; 32 | import rx.schedulers.Schedulers; 33 | import rx.schedulers.TestScheduler; 34 | import rx.subjects.TestSubject; 35 | 36 | public class HystrixStreamSource { 37 | 38 | public static final String STREAM_ALL = "hystrix"; 39 | public static final String STREAM_SUBSCRIBER = "hystrix-subscriber"; 40 | public static final String STREAM_CINEMATCH = "hystrix-cinematch"; 41 | public static final String STREAM_SUBSCRIBER_CINEMATCH_1 = "hystrix-subscriber_cinematch_1"; 42 | public static final String STREAM_SUBSCRIBER_CINEMATCH_2 = "hystrix-subscriber_cinematch_2"; 43 | 44 | public static void main(String[] args) { 45 | getHystrixStreamFromFile(STREAM_ALL, 12345, 1).take(5).toBlocking().forEach(new Action1>() { 46 | 47 | @Override 48 | public void call(Map s) { 49 | System.out.println("s: " + s.keySet()); 50 | } 51 | 52 | }); 53 | } 54 | 55 | // a hack to simulate a stream 56 | public static GroupedObservable> getHystrixStreamFromFile(final String stream, final int instanceID, int latencyBetweenEvents) { 57 | return GroupedObservable.from(InstanceKey.create(instanceID), Observable.create(new OnSubscribe>() { 58 | 59 | @Override 60 | public void call(Subscriber> sub) { 61 | try { 62 | while (!sub.isUnsubscribed()) { 63 | String packagePath = HystrixStreamSource.class.getPackage().getName().replace('.', '/'); 64 | InputStream file = HystrixStreamSource.class.getResourceAsStream("/" + packagePath + "/" + stream + ".stream"); 65 | BufferedReader in = new BufferedReader(new InputStreamReader(file)); 66 | String line = null; 67 | while ((line = in.readLine()) != null && !sub.isUnsubscribed()) { 68 | if (!line.trim().equals("")) { 69 | if (line.startsWith("data: ")) { 70 | String json = line.substring(6); 71 | try { 72 | Map jsonMap = JsonUtility.jsonToMap(json); 73 | jsonMap.put("instanceId", String.valueOf(instanceID)); 74 | sub.onNext(jsonMap); 75 | Thread.sleep(latencyBetweenEvents); 76 | } catch (Exception e) { 77 | e.printStackTrace(); 78 | } 79 | } 80 | } 81 | } 82 | } 83 | } catch (Exception e) { 84 | sub.onError(e); 85 | } 86 | } 87 | 88 | }).subscribeOn(Schedulers.newThread())); 89 | } 90 | 91 | public static GroupedObservable> getHystrixStreamFromFileEachLineScheduledEvery10Milliseconds(final String stream, final int instanceID, final TestScheduler scheduler, int maxTime) { 92 | TestSubject> scheduledOrigin = TestSubject.create(scheduler); 93 | try { 94 | String packagePath = HystrixStreamSource.class.getPackage().getName().replace('.', '/'); 95 | InputStream file = HystrixStreamSource.class.getResourceAsStream("/" + packagePath + "/" + stream + ".stream"); 96 | BufferedReader in = new BufferedReader(new InputStreamReader(file)); 97 | String line = null; 98 | int time = 0; 99 | while ((line = in.readLine()) != null && time < maxTime) { 100 | if (!line.trim().equals("")) { 101 | if (line.startsWith("data: ")) { 102 | time = time + 10; // increment by 10 milliseconds 103 | String json = line.substring(6); 104 | try { 105 | Map jsonMap = JsonUtility.jsonToMap(json); 106 | // System.err.println(instanceID + " => scheduling at time: " + time + " => " + jsonMap); 107 | scheduledOrigin.onNext(jsonMap, time); 108 | } catch (Exception e) { 109 | System.err.println("bad data"); 110 | } 111 | } 112 | } 113 | } 114 | scheduledOrigin.onCompleted(maxTime); 115 | } catch (Exception e) { 116 | throw new RuntimeException(e); 117 | } 118 | 119 | return GroupedObservable.from(InstanceKey.create(instanceID), scheduledOrigin.subscribeOn(scheduler)); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /turbine-core/src/test/java/com/netflix/turbine/aggregator/AggregateStringTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.aggregator; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | 20 | import org.junit.Test; 21 | 22 | public class AggregateStringTest { 23 | 24 | @Test 25 | public void testSingleValue() { 26 | AggregateString v = AggregateString.create("false", InstanceKey.create(1)); 27 | assertEquals("{\"false\":1}", v.toJson()); 28 | 29 | v = v.update("false", "false", InstanceKey.create(1)); 30 | assertEquals("{\"false\":1}", v.toJson()); 31 | 32 | v = v.update("false", "true", InstanceKey.create(1)); 33 | assertEquals("{\"true\":1}", v.toJson()); 34 | } 35 | 36 | @Test 37 | public void testMultipleValuesOnSingleInstance() { 38 | AggregateString v = AggregateString.create(); 39 | InstanceKey instance = InstanceKey.create(1); 40 | v = v.update(null, "false", instance); 41 | assertEquals("{\"false\":1}", v.toJson()); 42 | 43 | v = v.update("false", "false", instance); 44 | assertEquals("{\"false\":1}", v.toJson()); 45 | 46 | v = v.update("false", "true", instance); 47 | assertEquals("{\"true\":1}", v.toJson()); 48 | } 49 | 50 | @Test 51 | public void testMultipleValuesOnMultipleInstances() { 52 | AggregateString v = AggregateString.create(); 53 | v = v.update(null, "false", InstanceKey.create(1)); 54 | v = v.update(null, "true", InstanceKey.create(2)); 55 | assertEquals("{\"false\":1,\"true\":1}", v.toJson()); 56 | 57 | v = v.update("false", "false", InstanceKey.create(1)); // old value set so we'll decrement ... but same so no different 58 | assertEquals("{\"false\":1,\"true\":1}", v.toJson()); 59 | assertEquals(2, v.instances().size()); 60 | 61 | v = v.update(null, "false", InstanceKey.create(3)); 62 | assertEquals("{\"false\":2,\"true\":1}", v.toJson()); 63 | assertEquals(3, v.instances().size()); 64 | 65 | v = v.update("false", "true", InstanceKey.create(1)); 66 | assertEquals("{\"false\":1,\"true\":2}", v.toJson()); 67 | 68 | v = v.update("false", "true", InstanceKey.create(3)); 69 | assertEquals("{\"true\":3}", v.toJson()); 70 | 71 | // remove an instance 72 | v = v.update("true", null, InstanceKey.create(1)); 73 | assertEquals("{\"true\":2}", v.toJson()); 74 | assertEquals(2, v.instances().size()); 75 | 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /turbine-core/src/test/java/com/netflix/turbine/aggregator/NumberListTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.aggregator; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | 20 | import java.util.LinkedHashMap; 21 | 22 | import org.junit.Test; 23 | 24 | import com.netflix.turbine.aggregator.NumberList; 25 | 26 | public class NumberListTest { 27 | 28 | private static final LinkedHashMap values = new LinkedHashMap(); 29 | private static final LinkedHashMap values2 = new LinkedHashMap(); 30 | static { 31 | values.put("0", 10); 32 | values.put("25", 50); 33 | values.put("50", 100); 34 | values.put("75", 250); 35 | values.put("99", 500); 36 | 37 | values2.put("0", 15); 38 | values2.put("25", 55); 39 | values2.put("50", 105); 40 | values2.put("75", 255); 41 | values2.put("99", 505); 42 | } 43 | 44 | @Test 45 | public void testCreate() { 46 | NumberList nl = NumberList.create(values); 47 | System.out.println(nl); 48 | assertEquals(Long.valueOf(10), nl.get("0")); 49 | assertEquals(Long.valueOf(50), nl.get("25")); 50 | assertEquals(Long.valueOf(100), nl.get("50")); 51 | assertEquals(Long.valueOf(250), nl.get("75")); 52 | assertEquals(Long.valueOf(500), nl.get("99")); 53 | } 54 | 55 | @Test 56 | public void testDelta() { 57 | NumberList nl = NumberList.delta(values2, values); 58 | System.out.println(nl); 59 | assertEquals(Long.valueOf(5), nl.get("0")); 60 | assertEquals(Long.valueOf(5), nl.get("25")); 61 | assertEquals(Long.valueOf(5), nl.get("50")); 62 | assertEquals(Long.valueOf(5), nl.get("75")); 63 | assertEquals(Long.valueOf(5), nl.get("99")); 64 | } 65 | 66 | @Test 67 | public void testDeltaWithNumberList() { 68 | NumberList nl = NumberList.delta(NumberList.create(values2), NumberList.create(values)); 69 | System.out.println(nl); 70 | assertEquals(Long.valueOf(5), nl.get("0")); 71 | assertEquals(Long.valueOf(5), nl.get("25")); 72 | assertEquals(Long.valueOf(5), nl.get("50")); 73 | assertEquals(Long.valueOf(5), nl.get("75")); 74 | assertEquals(Long.valueOf(5), nl.get("99")); 75 | } 76 | 77 | @Test 78 | public void testSum() { 79 | NumberList nl = NumberList.create(values).sum(NumberList.create(values2)); 80 | System.out.println(nl); 81 | assertEquals(Long.valueOf(25), nl.get("0")); 82 | assertEquals(Long.valueOf(105), nl.get("25")); 83 | assertEquals(Long.valueOf(205), nl.get("50")); 84 | assertEquals(Long.valueOf(505), nl.get("75")); 85 | assertEquals(Long.valueOf(1005), nl.get("99")); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /turbine-core/src/test/java/com/netflix/turbine/aggregator/StreamAggregatorTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.aggregator; 17 | 18 | import static org.junit.Assert.assertArrayEquals; 19 | import static org.junit.Assert.assertEquals; 20 | 21 | import java.util.Arrays; 22 | import java.util.Collections; 23 | import java.util.HashMap; 24 | import java.util.LinkedHashMap; 25 | import java.util.List; 26 | import java.util.Map; 27 | import java.util.concurrent.TimeUnit; 28 | import java.util.concurrent.atomic.AtomicInteger; 29 | import java.util.stream.Collectors; 30 | 31 | import org.junit.Test; 32 | 33 | import rx.Observable; 34 | import rx.observables.GroupedObservable; 35 | import rx.observers.TestSubscriber; 36 | import rx.schedulers.TestScheduler; 37 | import rx.subjects.TestSubject; 38 | 39 | import com.netflix.turbine.HystrixStreamSource; 40 | 41 | public class StreamAggregatorTest { 42 | /** 43 | * Submit 3 events containing `rollingCountSuccess` of => 327, 370, 358 44 | * 45 | * We should receive a GroupedObservable of key "CinematchGetPredictions" with deltas => 327, 43, -12, -358 (onComplete) 46 | */ 47 | @Test 48 | public void testNumberValue_OneInstanceOneGroup() { 49 | TestScheduler scheduler = new TestScheduler(); 50 | TestSubject>> stream = TestSubject.create(scheduler); 51 | 52 | AtomicInteger numGroups = new AtomicInteger(); 53 | TestSubscriber ts = new TestSubscriber<>(); 54 | 55 | StreamAggregator.aggregateGroupedStreams(stream).flatMap(commandGroup -> { 56 | System.out.println("======> Got group for command: " + commandGroup.getKey()); 57 | numGroups.incrementAndGet(); 58 | return commandGroup.map(data -> { 59 | return data.get("rollingCountSuccess"); 60 | }); 61 | }).subscribe(ts); 62 | 63 | stream.onNext(getCinematchCommandInstanceStream(12345, scheduler), 5); 64 | stream.onCompleted(100); 65 | 66 | scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); 67 | 68 | ts.awaitTerminalEvent(); 69 | 70 | System.out.println("---------> OnErrorEvents: " + ts.getOnErrorEvents()); 71 | if (ts.getOnErrorEvents().size() > 0) { 72 | ts.getOnErrorEvents().get(0).printStackTrace(); 73 | } 74 | System.out.println("---------> OnNextEvents: " + ts.getOnNextEvents()); 75 | ts.assertNoErrors(); 76 | assertEquals(0, ts.getOnErrorEvents().size()); 77 | // we expect a single instance 78 | assertEquals(1, numGroups.get()); 79 | // the expected deltas for rollingCountSuccess 80 | ts.assertReceivedOnNext(Arrays.asList(327L, 370L, 358L, 0L)); 81 | } 82 | 83 | /** 84 | * Group 1: 327, 370, 358 => deltas: 327, 43, -12, -358 (onComplete) 85 | * Group 2: 617, 614, 585 => deltas: 617, -3, -29, -585 (onComplete) 86 | * 87 | * 88 | */ 89 | @Test 90 | public void testNumberValue_OneInstanceTwoGroups() { 91 | TestScheduler scheduler = new TestScheduler(); 92 | TestSubject>> stream = TestSubject.create(scheduler); 93 | 94 | AtomicInteger numGroups = new AtomicInteger(); 95 | TestSubscriber ts = new TestSubscriber<>(); 96 | 97 | StreamAggregator.aggregateGroupedStreams(stream).flatMap(commandGroup -> { 98 | System.out.println("======> Got group for command: " + commandGroup.getKey()); 99 | numGroups.incrementAndGet(); 100 | return commandGroup.map(data -> { 101 | return data.get("rollingCountSuccess"); 102 | }); 103 | }).subscribe(ts); 104 | 105 | stream.onNext(getSubscriberAndCinematchCommandInstanceStream(12345, scheduler), 0); 106 | stream.onCompleted(100); 107 | 108 | scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); 109 | 110 | ts.awaitTerminalEvent(); 111 | 112 | System.out.println("---------> OnErrorEvents: " + ts.getOnErrorEvents()); 113 | if (ts.getOnErrorEvents().size() > 0) { 114 | ts.getOnErrorEvents().get(0).printStackTrace(); 115 | } 116 | System.out.println("---------> OnNextEvents: " + ts.getOnNextEvents()); 117 | assertEquals(0, ts.getOnErrorEvents().size()); 118 | // we expect 2 commands 119 | assertEquals(2, numGroups.get()); 120 | // the expected deltas for rollingCountSuccess (2 instances of same data grouped together) 121 | ts.assertReceivedOnNext(Arrays.asList(327L, 617L, 370L, 614L, 358L, 585L, 0L, 0L)); // two 0s because both groups complete and remove themselves 122 | } 123 | 124 | /** 125 | * Two instances emitting: 327, 370, 358 => deltas: 327, 43, -12, -358 (onComplete) 126 | * 127 | * 327, 327, 370, 370, 358, 358 128 | * 129 | * 0 + 327 = 327 130 | * 327 + 327 = 654 131 | * 654 + 43 = 697 132 | * 697 + 43 = 740 133 | * 740 - 12 = 728 134 | * 728 - 358 = 370 135 | * 370 - 12 = 358 136 | * 358 - 358 = 0 137 | * 138 | */ 139 | @Test 140 | public void testNumberValue_TwoInstancesOneGroup() { 141 | TestScheduler scheduler = new TestScheduler(); 142 | TestSubject>> stream = TestSubject.create(scheduler); 143 | 144 | AtomicInteger numGroups = new AtomicInteger(); 145 | TestSubscriber ts = new TestSubscriber<>(); 146 | 147 | StreamAggregator.aggregateGroupedStreams(stream).flatMap(commandGroup -> { 148 | System.out.println("======> Got group for command: " + commandGroup.getKey()); 149 | numGroups.incrementAndGet(); 150 | return commandGroup.map(data -> { 151 | return data.get("rollingCountSuccess"); 152 | }); 153 | }).subscribe(ts); 154 | 155 | stream.onNext(getCinematchCommandInstanceStream(12345, scheduler), 0); 156 | stream.onNext(getCinematchCommandInstanceStream(23456, scheduler), 0); 157 | stream.onCompleted(100); 158 | 159 | scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); 160 | 161 | ts.awaitTerminalEvent(); 162 | 163 | System.out.println("---------> OnErrorEvents: " + ts.getOnErrorEvents()); 164 | if (ts.getOnErrorEvents().size() > 0) { 165 | ts.getOnErrorEvents().get(0).printStackTrace(); 166 | } 167 | System.out.println("---------> OnNextEvents: " + ts.getOnNextEvents()); 168 | assertEquals(0, ts.getOnErrorEvents().size()); 169 | // we expect 1 command 170 | assertEquals(1, numGroups.get()); 171 | // the expected deltas for rollingCountSuccess (2 instances of same data grouped together) 172 | ts.assertReceivedOnNext(Arrays.asList(327L, 654L, 697L, 740L, 728L, 370L, 358L, 0L)); 173 | } 174 | 175 | /** 176 | * 177 | * Each instance emits => 178 | * 179 | * Group 1: 327, 370, 358 => deltas: 327, 43, -12, -358 (onComplete) 180 | * Group 2: 617, 614, 585 => deltas: 617, -3, -29, -585 (onComplete) 181 | * 182 | * Group1 => 183 | * 184 | * 327, 327, 370, 370, 358, 358 185 | * 186 | * 0 + 327 = 327 187 | * 327 + 327 = 654 188 | * 654 + 43 = 697 189 | * 697 + 43 = 740 190 | * 740 - 12 = 728 191 | * 728 - 358 = 370 192 | * 370 - 12 = 358 193 | * 358 - 358 = 0 194 | * 195 | * Group 2 => 196 | * 197 | * 617, 617, 614, 614, 585, 585 198 | * 199 | * 0 + 617 = 617 200 | * 617 + 617 = 1234 201 | * 1234 - 3 = 1231 202 | * 1231 - 3 = 1228 203 | * 1228 - 29 = 1199 204 | * 1199 - 585 = 614 205 | * 614 - 29 = 585 206 | * 585 - 585 = 0 207 | * 208 | * Interleaved because 2 groups: 209 | * 210 | * 327, 654, 617, 1234, 697, 740, 1231, 1228, 728, 716, 1199, 1170 211 | * 212 | */ 213 | @Test 214 | public void testNumberValue_TwoInstancesTwoGroups() { 215 | TestScheduler scheduler = new TestScheduler(); 216 | TestSubject>> stream = TestSubject.create(scheduler); 217 | 218 | AtomicInteger numGroups = new AtomicInteger(); 219 | TestSubscriber ts = new TestSubscriber<>(); 220 | 221 | StreamAggregator.aggregateGroupedStreams(stream).flatMap(commandGroup -> { 222 | System.out.println("======> Got group for command: " + commandGroup.getKey()); 223 | numGroups.incrementAndGet(); 224 | return commandGroup.map(data -> { 225 | return data.get("rollingCountSuccess"); 226 | }); 227 | }).subscribe(ts); 228 | 229 | stream.onNext(getSubscriberAndCinematchCommandInstanceStream(12345, scheduler), 0); 230 | stream.onNext(getSubscriberAndCinematchCommandInstanceStream(23456, scheduler), 5); 231 | stream.onCompleted(100); 232 | 233 | scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); 234 | 235 | ts.awaitTerminalEvent(); 236 | 237 | System.out.println("---------> OnErrorEvents: " + ts.getOnErrorEvents()); 238 | if (ts.getOnErrorEvents().size() > 0) { 239 | ts.getOnErrorEvents().get(0).printStackTrace(); 240 | } 241 | System.out.println("---------> OnNextEvents: " + ts.getOnNextEvents()); 242 | assertEquals(0, ts.getOnErrorEvents().size()); 243 | // we expect 2 commands 244 | assertEquals(2, numGroups.get()); 245 | // the expected deltas for rollingCountSuccess (2 instances of same data grouped together) 246 | ts.assertReceivedOnNext(Arrays.asList(327L, 654L, 617L, 1234L, 697L, 740L, 1231L, 1228L, 728L, 716L, 1199L, 614L, 358L, 585L, 0L, 0L)); 247 | } 248 | 249 | @Test 250 | public void testStringValue_OneInstanceOneGroup() { 251 | TestScheduler scheduler = new TestScheduler(); 252 | TestSubject>> stream = TestSubject.create(scheduler); 253 | 254 | AtomicInteger numGroups = new AtomicInteger(); 255 | TestSubscriber ts = new TestSubscriber<>(); 256 | 257 | StreamAggregator.aggregateGroupedStreams(stream).flatMap(commandGroup -> { 258 | System.out.println("======> Got group for command: " + commandGroup.getKey()); 259 | numGroups.incrementAndGet(); 260 | return commandGroup.map(data -> { 261 | return ((AggregateString) data.get("isCircuitBreakerOpen")).toJson(); 262 | }); 263 | }).subscribe(ts); 264 | 265 | stream.onNext(getCinematchCommandInstanceStream(12345, scheduler), 5); 266 | stream.onCompleted(100); 267 | 268 | scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); 269 | 270 | ts.awaitTerminalEvent(); 271 | 272 | System.out.println("---------> OnErrorEvents: " + ts.getOnErrorEvents()); 273 | if (ts.getOnErrorEvents().size() > 0) { 274 | ts.getOnErrorEvents().get(0).printStackTrace(); 275 | } 276 | System.out.println("---------> OnNextEvents: " + ts.getOnNextEvents()); 277 | assertEquals(0, ts.getOnErrorEvents().size()); 278 | // we expect a single instance 279 | assertEquals(1, numGroups.get()); 280 | // the expected deltas for rollingCountSuccess 281 | ts.assertReceivedOnNext(Arrays.asList("{\"false\":1}", "{\"false\":1}", "{\"true\":1}", "{}")); 282 | } 283 | 284 | @Test 285 | public void testStringValue_TwoInstancesOneGroup() { 286 | TestScheduler scheduler = new TestScheduler(); 287 | TestSubject>> stream = TestSubject.create(scheduler); 288 | 289 | AtomicInteger numGroups = new AtomicInteger(); 290 | TestSubscriber ts = new TestSubscriber<>(); 291 | 292 | StreamAggregator.aggregateGroupedStreams(stream).flatMap(commandGroup -> { 293 | System.out.println("======> Got group for command: " + commandGroup.getKey()); 294 | numGroups.incrementAndGet(); 295 | return commandGroup.map(data -> { 296 | return ((AggregateString) data.get("isCircuitBreakerOpen")).toJson(); 297 | }); 298 | }).subscribe(ts); 299 | 300 | stream.onNext(getCinematchCommandInstanceStream(12345, scheduler), 0); 301 | stream.onNext(getCinematchCommandInstanceStream(23456, scheduler), 0); 302 | stream.onCompleted(100); 303 | 304 | scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); 305 | 306 | ts.awaitTerminalEvent(); 307 | 308 | System.out.println("---------> OnErrorEvents: " + ts.getOnErrorEvents()); 309 | if (ts.getOnErrorEvents().size() > 0) { 310 | ts.getOnErrorEvents().get(0).printStackTrace(); 311 | } 312 | System.out.println("---------> OnNextEvents: " + ts.getOnNextEvents()); 313 | assertEquals(0, ts.getOnErrorEvents().size()); 314 | // we expect 1 command 315 | assertEquals(1, numGroups.get()); 316 | // the expected deltas for rollingCountSuccess (2 instances of same data grouped together) 317 | ts.assertReceivedOnNext(Arrays.asList("{\"false\":1}", "{\"false\":2}", "{\"false\":2}", "{\"false\":2}", "{\"false\":1,\"true\":1}", "{\"false\":1}", "{\"true\":1}", "{}")); 318 | } 319 | 320 | /* 321 | * Test that an instance dropping correctly removes the data 322 | */ 323 | @Test 324 | public void testInstanceRemovalStringValue() { 325 | TestScheduler scheduler = new TestScheduler(); 326 | TestSubject>> stream = TestSubject.create(scheduler); 327 | 328 | AtomicInteger numGroups = new AtomicInteger(); 329 | TestSubscriber ts = new TestSubscriber<>(); 330 | 331 | StreamAggregator.aggregateGroupedStreams(stream). flatMap(commandGroup -> { 332 | System.out.println("======> Got group for command: " + commandGroup.getKey()); 333 | numGroups.incrementAndGet(); 334 | return commandGroup.map(data -> { 335 | return ((AggregateString) data.get("isCircuitBreakerOpen")); 336 | }); 337 | }).subscribe(ts); 338 | 339 | stream.onNext(getCinematchCommandInstanceStream(12345, scheduler, 31), 0); 340 | stream.onNext(getCinematchCommandInstanceStream(23456, scheduler, 100), 0); 341 | stream.onCompleted(100); 342 | 343 | scheduler.advanceTimeTo(30, TimeUnit.MILLISECONDS); 344 | 345 | // assert we have two groups aggregated 346 | 347 | List onNextAt30 = ts.getOnNextEvents(); 348 | List jsonAt30 = ts.getOnNextEvents().stream().map(as -> as.toJson()).collect(Collectors.toList()); 349 | System.out.println("OnNext at 30ms -> " + jsonAt30); 350 | 351 | // we should have 2 instance now 352 | System.out.println("Instances at 30: " + onNextAt30.get(onNextAt30.size() - 1).instances()); 353 | assertEquals(2, onNextAt30.get(onNextAt30.size() - 1).instances().size()); 354 | // the expected deltas for rollingCountSuccess (2 instances of same data grouped together) 355 | assertEquals(jsonAt30, Arrays.asList("{\"false\":1}", "{\"false\":2}", "{\"false\":2}", "{\"false\":2}", "{\"false\":1,\"true\":1}", "{\"true\":2}")); 356 | 357 | // advance past the first stream so it onCompletes and removes itself 358 | scheduler.advanceTimeTo(31, TimeUnit.MILLISECONDS); 359 | // we should now see only 1 value 360 | List onNextAt31 = ts.getOnNextEvents(); 361 | List jsonAt31 = ts.getOnNextEvents().stream().map(as -> as.toJson()).collect(Collectors.toList()); 362 | System.out.println("OnNext at 31ms -> " + jsonAt31); 363 | 364 | // we should only have 1 instance now 365 | System.out.println("Instances at 31: " + onNextAt31.get(onNextAt31.size() - 1).instances()); 366 | assertEquals(1, onNextAt31.get(onNextAt31.size() - 1).instances().size()); 367 | 368 | assertEquals(jsonAt31, Arrays.asList("{\"false\":1}", "{\"false\":2}", "{\"false\":2}", "{\"false\":2}", "{\"false\":1,\"true\":1}", "{\"true\":2}", "{\"true\":1}")); 369 | 370 | // complete 371 | scheduler.advanceTimeTo(100, TimeUnit.MILLISECONDS); 372 | 373 | ts.awaitTerminalEvent(); 374 | 375 | System.out.println("---------> OnErrorEvents: " + ts.getOnErrorEvents()); 376 | if (ts.getOnErrorEvents().size() > 0) { 377 | ts.getOnErrorEvents().get(0).printStackTrace(); 378 | } 379 | System.out.println("---------> OnNextEvents: " + ts.getOnNextEvents()); 380 | assertEquals(0, ts.getOnErrorEvents().size()); 381 | // we expect 1 command 382 | assertEquals(1, numGroups.get()); 383 | } 384 | 385 | @Test 386 | public void testFields() { 387 | TestScheduler scheduler = new TestScheduler(); 388 | TestSubject>> stream = TestSubject.create(scheduler); 389 | 390 | AtomicInteger numGroups = new AtomicInteger(); 391 | TestSubscriber ts = new TestSubscriber<>(); 392 | 393 | StreamAggregator.aggregateGroupedStreams(stream).flatMap(commandGroup -> { 394 | numGroups.incrementAndGet(); 395 | return commandGroup.map(data -> { 396 | validateNumber(data, "reportingHosts"); 397 | validateString(data, "type"); 398 | validateString(data, "name"); 399 | validateAggregateString(data, "group"); 400 | validateNull(data, "currentTime"); 401 | validateAggregateString(data, "isCircuitBreakerOpen"); 402 | validateNumber(data, "errorPercentage"); 403 | validateNumber(data, "errorCount"); 404 | validateNumber(data, "requestCount"); 405 | validateNumber(data, "rollingCountCollapsedRequests"); 406 | validateNumber(data, "rollingCountExceptionsThrown"); 407 | validateNumber(data, "rollingCountFailure"); 408 | validateNumber(data, "rollingCountFallbackFailure"); 409 | validateNumber(data, "rollingCountFallbackRejection"); 410 | validateNumber(data, "rollingCountFallbackSuccess"); 411 | validateNumber(data, "rollingCountResponsesFromCache"); 412 | validateNumber(data, "rollingCountSemaphoreRejected"); 413 | validateNumber(data, "rollingCountShortCircuited"); 414 | validateNumber(data, "rollingCountSuccess"); 415 | validateNumber(data, "rollingCountThreadPoolRejected"); 416 | validateNumber(data, "rollingCountTimeout"); 417 | validateNumber(data, "currentConcurrentExecutionCount"); 418 | validateNumber(data, "latencyExecute_mean"); 419 | validateNumberList(data, "latencyExecute"); 420 | validateNumber(data, "latencyTotal_mean"); 421 | validateNumberList(data, "latencyTotal"); 422 | validateAggregateString(data, "propertyValue_circuitBreakerRequestVolumeThreshold"); 423 | validateAggregateString(data, "propertyValue_circuitBreakerSleepWindowInMilliseconds"); 424 | validateAggregateString(data, "propertyValue_circuitBreakerErrorThresholdPercentage"); 425 | validateAggregateString(data, "propertyValue_circuitBreakerForceOpen"); 426 | validateAggregateString(data, "propertyValue_executionIsolationStrategy"); 427 | validateAggregateString(data, "propertyValue_executionIsolationThreadTimeoutInMilliseconds"); 428 | validateAggregateString(data, "propertyValue_executionIsolationThreadInterruptOnTimeout"); 429 | validateAggregateString(data, "propertyValue_executionIsolationSemaphoreMaxConcurrentRequests"); 430 | validateAggregateString(data, "propertyValue_fallbackIsolationSemaphoreMaxConcurrentRequests"); 431 | validateAggregateString(data, "propertyValue_requestCacheEnabled"); 432 | validateAggregateString(data, "propertyValue_requestLogEnabled"); 433 | validateAggregateString(data, "propertyValue_metricsRollingStatisticalWindowInMilliseconds"); 434 | return data.get("name"); 435 | }); 436 | }).subscribe(ts); 437 | 438 | stream.onNext(getCinematchCommandInstanceStream(12345, scheduler), 0); 439 | stream.onNext(getCinematchCommandInstanceStream(23456, scheduler), 0); 440 | stream.onCompleted(100); 441 | 442 | scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); 443 | 444 | ts.awaitTerminalEvent(); 445 | 446 | System.out.println("---------> OnErrorEvents: " + ts.getOnErrorEvents()); 447 | if (ts.getOnErrorEvents().size() > 0) { 448 | ts.getOnErrorEvents().get(0).printStackTrace(); 449 | } 450 | System.out.println("---------> OnNextEvents: " + ts.getOnNextEvents()); 451 | assertEquals(0, ts.getOnErrorEvents().size()); 452 | // we expect 1 command 453 | assertEquals(1, numGroups.get()); 454 | // the expected deltas for rollingCountSuccess (2 instances of same data grouped together) 455 | ts.assertReceivedOnNext(Arrays.asList("CinematchGetPredictions", "CinematchGetPredictions", "CinematchGetPredictions", "CinematchGetPredictions", "CinematchGetPredictions", "CinematchGetPredictions", "CinematchGetPredictions", "CinematchGetPredictions")); 456 | } 457 | 458 | @Test 459 | public void testFieldReportingHosts() { 460 | TestScheduler scheduler = new TestScheduler(); 461 | TestSubject>> stream = TestSubject.create(scheduler); 462 | 463 | AtomicInteger numGroups = new AtomicInteger(); 464 | TestSubscriber ts = new TestSubscriber<>(); 465 | 466 | StreamAggregator.aggregateGroupedStreams(stream).flatMap(commandGroup -> { 467 | numGroups.incrementAndGet(); 468 | return commandGroup.map(data -> { 469 | return data.get("reportingHosts"); 470 | }); 471 | }).subscribe(ts); 472 | 473 | stream.onNext(getCinematchCommandInstanceStream(12345, scheduler), 0); 474 | stream.onNext(getCinematchCommandInstanceStream(23456, scheduler), 0); 475 | stream.onCompleted(100); 476 | 477 | scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); 478 | 479 | ts.awaitTerminalEvent(); 480 | 481 | System.out.println("---------> OnErrorEvents: " + ts.getOnErrorEvents()); 482 | if (ts.getOnErrorEvents().size() > 0) { 483 | ts.getOnErrorEvents().get(0).printStackTrace(); 484 | } 485 | System.out.println("---------> OnNextEvents: " + ts.getOnNextEvents()); 486 | assertEquals(0, ts.getOnErrorEvents().size()); 487 | // we expect 1 command 488 | assertEquals(1, numGroups.get()); 489 | ts.assertReceivedOnNext(Arrays.asList(1L, 2L, 2L, 2L, 2L, 1L, 1L, 0L)); 490 | } 491 | 492 | @Test 493 | public void testField_propertyValue_circuitBreakerForceOpen() { 494 | TestScheduler scheduler = new TestScheduler(); 495 | TestSubject>> stream = TestSubject.create(scheduler); 496 | 497 | AtomicInteger numGroups = new AtomicInteger(); 498 | TestSubscriber ts = new TestSubscriber<>(); 499 | 500 | StreamAggregator.aggregateGroupedStreams(stream).flatMap(commandGroup -> { 501 | numGroups.incrementAndGet(); 502 | return commandGroup.map(data -> { 503 | return String.valueOf(data.get("propertyValue_circuitBreakerForceOpen")); 504 | }); 505 | }).subscribe(ts); 506 | 507 | stream.onNext(getCinematchCommandInstanceStream(12345, scheduler), 0); 508 | stream.onNext(getCinematchCommandInstanceStream(23456, scheduler), 0); 509 | stream.onCompleted(100); 510 | 511 | scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); 512 | 513 | ts.awaitTerminalEvent(); 514 | 515 | System.out.println("---------> OnErrorEvents: " + ts.getOnErrorEvents()); 516 | if (ts.getOnErrorEvents().size() > 0) { 517 | ts.getOnErrorEvents().get(0).printStackTrace(); 518 | } 519 | System.out.println("---------> OnNextEvents: " + ts.getOnNextEvents()); 520 | assertEquals(0, ts.getOnErrorEvents().size()); 521 | // we expect 1 command 522 | assertEquals(1, numGroups.get()); 523 | ts.assertReceivedOnNext(Arrays.asList("AggregateString => {\"false\":1}", 524 | "AggregateString => {\"false\":2}", 525 | "AggregateString => {\"false\":2}", 526 | "AggregateString => {\"false\":2}", 527 | "AggregateString => {\"false\":2}", 528 | "AggregateString => {\"false\":1}", 529 | "AggregateString => {\"false\":1}", 530 | "AggregateString => {}")); 531 | } 532 | 533 | @Test 534 | public void testFieldOnStream() { 535 | TestScheduler scheduler = new TestScheduler(); 536 | TestSubscriber ts = new TestSubscriber<>(); 537 | // 20 events per instance, 10 per group 538 | // 80 events total 539 | GroupedObservable> hystrixStreamA = HystrixStreamSource.getHystrixStreamFromFileEachLineScheduledEvery10Milliseconds(HystrixStreamSource.STREAM_SUBSCRIBER_CINEMATCH_1, 12345, scheduler, 200); 540 | GroupedObservable> hystrixStreamB = HystrixStreamSource.getHystrixStreamFromFileEachLineScheduledEvery10Milliseconds(HystrixStreamSource.STREAM_SUBSCRIBER_CINEMATCH_1, 23456, scheduler, 200); 541 | GroupedObservable> hystrixStreamC = HystrixStreamSource.getHystrixStreamFromFileEachLineScheduledEvery10Milliseconds(HystrixStreamSource.STREAM_SUBSCRIBER_CINEMATCH_1, 67890, scheduler, 200); 542 | GroupedObservable> hystrixStreamD = HystrixStreamSource.getHystrixStreamFromFileEachLineScheduledEvery10Milliseconds(HystrixStreamSource.STREAM_SUBSCRIBER_CINEMATCH_1, 63543, scheduler, 200); 543 | 544 | Observable>> fullStream = Observable.just(hystrixStreamA, hystrixStreamB, hystrixStreamC, hystrixStreamD); 545 | StreamAggregator.aggregateGroupedStreams(fullStream).flatMap(commandGroup -> { 546 | System.out.println("======> Got group for command: " + commandGroup.getKey()); 547 | return commandGroup; 548 | }).doOnNext(data -> { 549 | System.out.println("data => " + data.get("propertyValue_circuitBreakerForceOpen") + " " + data.get("name")); 550 | }).skip(8).doOnNext(v -> { 551 | // assert the count is always 4 (4 instances) on AggregateString values 552 | AggregateString as = (AggregateString) (v.get("propertyValue_circuitBreakerForceOpen")); 553 | if (!"AggregateString => {\"false\":4}".equals(as.toString())) { 554 | // after the initial 1, 2, 3, 4 counting on each instance we should receive 4 always thereafter 555 | // and we skip the first 8 to get past those 556 | throw new IllegalStateException("Expect the count to always be 4 but was " + as.toString()); 557 | } 558 | }).subscribe(ts); 559 | 560 | // only got to 199 so we don't trigger completion 561 | scheduler.advanceTimeBy(199, TimeUnit.MILLISECONDS); 562 | 563 | System.out.println("---------> OnErrorEvents: " + ts.getOnErrorEvents()); 564 | if (ts.getOnErrorEvents().size() > 0) { 565 | ts.getOnErrorEvents().get(0).printStackTrace(); 566 | } 567 | System.out.println("---------> OnNextEvents: " + ts.getOnNextEvents()); 568 | assertEquals(0, ts.getOnErrorEvents().size()); 569 | } 570 | 571 | private void validateNumberList(Map data, String key) { 572 | Object o = data.get(key); 573 | if (o == null) { 574 | throw new IllegalStateException("Expected value: " + key); 575 | } 576 | if (!(o instanceof NumberList)) { 577 | throw new IllegalStateException("Expected value of '" + key + "' to be a NumberList but was: " + o.getClass().getSimpleName()); 578 | } 579 | } 580 | 581 | private void validateNull(Map data, String key) { 582 | Object o = data.get(key); 583 | if (o != null) { 584 | throw new IllegalStateException("Did not expect value for key: " + key); 585 | } 586 | } 587 | 588 | private void validateAggregateString(Map data, String key) { 589 | Object o = data.get(key); 590 | if (o == null) { 591 | throw new IllegalStateException("Expected value: " + key); 592 | } 593 | if (!(o instanceof AggregateString)) { 594 | throw new IllegalStateException("Expected value of '" + key + "' to be a AggregateString but was: " + o.getClass().getSimpleName()); 595 | } 596 | } 597 | 598 | private void validateString(Map data, String key) { 599 | Object o = data.get(key); 600 | if (o == null) { 601 | throw new IllegalStateException("Expected value: " + key); 602 | } 603 | if (!(o instanceof String)) { 604 | throw new IllegalStateException("Expected value of '" + key + "' to be a String but was: " + o.getClass().getSimpleName()); 605 | } 606 | } 607 | 608 | private void validateNumber(Map data, String key) { 609 | Object o = data.get(key); 610 | if (o == null) { 611 | throw new IllegalStateException("Expected value: " + key); 612 | } 613 | if (!(o instanceof Number)) { 614 | throw new IllegalStateException("Expected value of '" + key + "' to be a Number but was: " + o.getClass().getSimpleName()); 615 | } 616 | } 617 | 618 | /** 619 | * This looks for the latency values which look like this: 620 | * 621 | * {"0":0,"25":0,"50":4,"75":11,"90":14,"95":17,"99":31,"99.5":43,"100":71} 622 | * {"0":0,"25":0,"50":3,"75":12,"90":17,"95":24,"99":48,"99.5":363,"100":390} 623 | * {"0":0,"25":0,"50":3,"75":12,"90":17,"95":24,"99":48,"99.5":363,"100":390} 624 | * 625 | * The inner values need to be summed. 626 | */ 627 | @Test 628 | public void testArrayMapValue_OneInstanceOneGroup() { 629 | TestScheduler scheduler = new TestScheduler(); 630 | TestSubject>> stream = TestSubject.create(scheduler); 631 | 632 | AtomicInteger numGroups = new AtomicInteger(); 633 | TestSubscriber ts = new TestSubscriber<>(); 634 | 635 | StreamAggregator.aggregateGroupedStreams(stream).flatMap(commandGroup -> { 636 | System.out.println("======> Got group for command: " + commandGroup.getKey()); 637 | numGroups.incrementAndGet(); 638 | return commandGroup.map(data -> { 639 | return ((NumberList) data.get("latencyTotal")).toJson(); 640 | }); 641 | }).subscribe(ts); 642 | 643 | stream.onNext(getCinematchCommandInstanceStream(12345, scheduler), 5); 644 | stream.onCompleted(100); 645 | 646 | scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); 647 | 648 | ts.awaitTerminalEvent(); 649 | 650 | System.out.println("---------> OnErrorEvents: " + ts.getOnErrorEvents()); 651 | if (ts.getOnErrorEvents().size() > 0) { 652 | ts.getOnErrorEvents().get(0).printStackTrace(); 653 | } 654 | System.out.println("---------> OnNextEvents: " + ts.getOnNextEvents()); 655 | assertEquals(0, ts.getOnErrorEvents().size()); 656 | // we expect a single instance 657 | assertEquals(1, numGroups.get()); 658 | // the expected deltas for rollingCountSuccess 659 | ts.assertReceivedOnNext(Arrays.asList("{\"0\":0,\"25\":0,\"50\":4,\"75\":11,\"90\":14,\"95\":17,\"99\":31,\"99.5\":43,\"100\":71}", "{\"0\":0,\"25\":0,\"50\":3,\"75\":12,\"90\":17,\"95\":24,\"99\":48,\"99.5\":363,\"100\":390}", "{\"0\":0,\"25\":0,\"50\":3,\"75\":12,\"90\":17,\"95\":24,\"99\":48,\"99.5\":363,\"100\":390}", "{\"0\":0,\"25\":0,\"50\":0,\"75\":0,\"90\":0,\"95\":0,\"99\":0,\"99.5\":0,\"100\":0}")); 660 | } 661 | 662 | /** 663 | * This looks for the latency values which look like this: 664 | * 665 | * {"0":0,"25":0,"50":4,"75":11,"90":14,"95":17,"99":31,"99.5":43,"100":71} 666 | * {"0":0,"25":0,"50":3,"75":12,"90":17,"95":24,"99":48,"99.5":363,"100":390} 667 | * {"0":0,"25":0,"50":3,"75":12,"90":17,"95":24,"99":48,"99.5":363,"100":390} 668 | * 669 | * The inner values need to be summed. 670 | */ 671 | @Test 672 | public void testArrayMapValue_TwoInstanceOneGroup() { 673 | TestScheduler scheduler = new TestScheduler(); 674 | TestSubject>> stream = TestSubject.create(scheduler); 675 | 676 | AtomicInteger numGroups = new AtomicInteger(); 677 | TestSubscriber ts = new TestSubscriber<>(); 678 | 679 | StreamAggregator.aggregateGroupedStreams(stream).flatMap(commandGroup -> { 680 | System.out.println("======> Got group for command: " + commandGroup.getKey()); 681 | numGroups.incrementAndGet(); 682 | return commandGroup.map(data -> { 683 | return ((NumberList) data.get("latencyTotal")).toJson(); 684 | }); 685 | }).subscribe(ts); 686 | 687 | stream.onNext(getCinematchCommandInstanceStream(12345, scheduler), 0); 688 | stream.onNext(getCinematchCommandInstanceStream(23456, scheduler), 5); 689 | stream.onCompleted(100); 690 | 691 | scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); 692 | 693 | ts.awaitTerminalEvent(); 694 | 695 | System.out.println("---------> OnErrorEvents: " + ts.getOnErrorEvents()); 696 | if (ts.getOnErrorEvents().size() > 0) { 697 | ts.getOnErrorEvents().get(0).printStackTrace(); 698 | } 699 | System.out.println("---------> OnNextEvents: " + ts.getOnNextEvents()); 700 | assertEquals(0, ts.getOnErrorEvents().size()); 701 | // we expect a single instance 702 | assertEquals(1, numGroups.get()); 703 | // the expected deltas for rollingCountSuccess 704 | ts.assertReceivedOnNext(Arrays.asList( 705 | "{\"0\":0,\"25\":0,\"50\":4,\"75\":11,\"90\":14,\"95\":17,\"99\":31,\"99.5\":43,\"100\":71}", 706 | "{\"0\":0,\"25\":0,\"50\":8,\"75\":22,\"90\":28,\"95\":34,\"99\":62,\"99.5\":86,\"100\":142}", // 71 + 71 combination 707 | "{\"0\":0,\"25\":0,\"50\":7,\"75\":23,\"90\":31,\"95\":41,\"99\":79,\"99.5\":406,\"100\":461}", // 71 + 390 combination 708 | "{\"0\":0,\"25\":0,\"50\":6,\"75\":24,\"90\":34,\"95\":48,\"99\":96,\"99.5\":726,\"100\":780}", // 390 + 390 combination 709 | "{\"0\":0,\"25\":0,\"50\":6,\"75\":24,\"90\":34,\"95\":48,\"99\":96,\"99.5\":726,\"100\":780}", // 390 + 390 combination 710 | "{\"0\":0,\"25\":0,\"50\":3,\"75\":12,\"90\":17,\"95\":24,\"99\":48,\"99.5\":363,\"100\":390}", // 780 - 390 711 | "{\"0\":0,\"25\":0,\"50\":3,\"75\":12,\"90\":17,\"95\":24,\"99\":48,\"99.5\":363,\"100\":390}", // 780 - 390 712 | "{\"0\":0,\"25\":0,\"50\":0,\"75\":0,\"90\":0,\"95\":0,\"99\":0,\"99.5\":0,\"100\":0}")); 713 | } 714 | 715 | private GroupedObservable> getCinematchCommandInstanceStream(int instanceId, TestScheduler scheduler) { 716 | return getCinematchCommandInstanceStream(instanceId, scheduler, 30); // 30ms max time before onComplete 717 | } 718 | 719 | // `rollingCountSuccess` of => 327, 370, 358 720 | private GroupedObservable> getCinematchCommandInstanceStream(int instanceId, TestScheduler scheduler, int time) { 721 | return HystrixStreamSource.getHystrixStreamFromFileEachLineScheduledEvery10Milliseconds(HystrixStreamSource.STREAM_CINEMATCH, instanceId, scheduler, time); 722 | } 723 | 724 | // `rollingCountSuccess` of => 617, 614, 585 725 | private GroupedObservable> getSubscriberCommandInstanceStream(int instanceId, TestScheduler scheduler) { 726 | return HystrixStreamSource.getHystrixStreamFromFileEachLineScheduledEvery10Milliseconds(HystrixStreamSource.STREAM_SUBSCRIBER, instanceId, scheduler, 30); 727 | } 728 | 729 | // `rollingCountSuccess` of => 327, 617, 370, 614, 358, 585 730 | private GroupedObservable> getSubscriberAndCinematchCommandInstanceStream(int instanceId, TestScheduler scheduler) { 731 | return HystrixStreamSource.getHystrixStreamFromFileEachLineScheduledEvery10Milliseconds(HystrixStreamSource.STREAM_SUBSCRIBER_CINEMATCH_1, instanceId, scheduler, 60); 732 | } 733 | 734 | private Map newMapInitializedWithInstanceKey() { 735 | Map m = new LinkedHashMap<>(); 736 | m.put("InstanceKey", InstanceKey.create(98765)); 737 | return m; 738 | } 739 | 740 | @Test 741 | public void testDeltaNumberNew() { 742 | Map mCurrent = newMapInitializedWithInstanceKey(); 743 | mCurrent.put("a", 1); 744 | mCurrent.put("b", 2); 745 | Map d = StreamAggregator.previousAndCurrentToDelta(Collections.emptyMap(), mCurrent); 746 | assertEquals(1l, d.get("a")); 747 | assertEquals(2l, d.get("b")); 748 | 749 | Map s = StreamAggregator.sumOfDelta(newMapInitializedWithInstanceKey(), d); 750 | assertEquals(1l, s.get("a")); 751 | assertEquals(2l, s.get("b")); 752 | } 753 | 754 | @Test 755 | public void testDeltaNumber1() { 756 | Map mPrevious = newMapInitializedWithInstanceKey(); 757 | mPrevious.put("a", 1); 758 | mPrevious.put("b", 2); 759 | 760 | Map mCurrent = newMapInitializedWithInstanceKey(); 761 | mCurrent.put("a", 3); 762 | mCurrent.put("b", 1); 763 | 764 | Map d = StreamAggregator.previousAndCurrentToDelta(mPrevious, mCurrent); 765 | assertEquals(2l, d.get("a")); 766 | assertEquals(-1l, d.get("b")); 767 | 768 | Map s = StreamAggregator.sumOfDelta(mPrevious, d); 769 | assertEquals(3l, s.get("a")); 770 | assertEquals(1l, s.get("b")); 771 | } 772 | 773 | @Test 774 | public void testDeltaNumber2() { 775 | Map mPrevious = newMapInitializedWithInstanceKey(); 776 | mPrevious.put("a", 4); 777 | mPrevious.put("b", 3); 778 | 779 | Map mCurrent = newMapInitializedWithInstanceKey(); 780 | mCurrent.put("a", 2); 781 | mCurrent.put("b", 2); 782 | 783 | Map d = StreamAggregator.previousAndCurrentToDelta(mPrevious, mCurrent); 784 | assertEquals(-2l, d.get("a")); 785 | assertEquals(-1l, d.get("b")); 786 | 787 | Map s = StreamAggregator.sumOfDelta(mPrevious, d); 788 | assertEquals(2l, s.get("a")); 789 | assertEquals(2l, s.get("b")); 790 | } 791 | 792 | @Test 793 | public void testDeltaNumberRemove() { 794 | Map mPrevious = newMapInitializedWithInstanceKey(); 795 | mPrevious.put("a", 4); 796 | mPrevious.put("b", 3); 797 | 798 | Map d = StreamAggregator.previousAndCurrentToDelta(mPrevious, Collections.emptyMap()); 799 | assertEquals(-4l, d.get("a")); 800 | assertEquals(-3l, d.get("b")); 801 | 802 | Map s = StreamAggregator.sumOfDelta(mPrevious, d); 803 | assertEquals(0l, s.get("a")); 804 | assertEquals(0l, s.get("b")); 805 | } 806 | 807 | @Test 808 | public void testDeltaNumberRemoveWithEmptyMapHavingInstanceKey() { 809 | Map mPrevious = newMapInitializedWithInstanceKey(); 810 | mPrevious.put("a", 4); 811 | mPrevious.put("b", 3); 812 | 813 | Map mCurrent = newMapInitializedWithInstanceKey(); 814 | 815 | Map d = StreamAggregator.previousAndCurrentToDelta(mPrevious, mCurrent); 816 | assertEquals(-4l, d.get("a")); 817 | assertEquals(-3l, d.get("b")); 818 | 819 | Map s = StreamAggregator.sumOfDelta(mPrevious, d); 820 | assertEquals(0l, s.get("a")); 821 | assertEquals(0l, s.get("b")); 822 | } 823 | 824 | @Test 825 | public void testDeltaBooleanNew() { 826 | Map mCurrent = newMapInitializedWithInstanceKey(); 827 | mCurrent.put("a", Boolean.TRUE); 828 | mCurrent.put("b", Boolean.FALSE); 829 | Map d = StreamAggregator.previousAndCurrentToDelta(Collections.emptyMap(), mCurrent); 830 | String[] as = (String[]) d.get("a"); 831 | String[] bs = (String[]) d.get("b"); 832 | assertArrayEquals(new String[] { "true" }, as); 833 | assertArrayEquals(new String[] { "false" }, bs); 834 | 835 | Map s = StreamAggregator.sumOfDelta(new LinkedHashMap<>(), d); 836 | assertEquals("AggregateString => {\"true\":1}", s.get("a").toString()); 837 | assertEquals("AggregateString => {\"false\":1}", s.get("b").toString()); 838 | } 839 | 840 | @Test 841 | public void testDeltaBoolean1() { 842 | Map mPrevious = newMapInitializedWithInstanceKey(); 843 | mPrevious.put("a", Boolean.TRUE); 844 | mPrevious.put("b", Boolean.FALSE); 845 | 846 | Map mCurrent = newMapInitializedWithInstanceKey(); 847 | mCurrent.put("a", Boolean.TRUE); 848 | mCurrent.put("b", Boolean.TRUE); 849 | 850 | Map d = StreamAggregator.previousAndCurrentToDelta(mPrevious, mCurrent); 851 | String[] as = (String[]) d.get("a"); 852 | String[] bs = (String[]) d.get("b"); 853 | assertArrayEquals(new String[] { "true", "true" }, as); 854 | assertArrayEquals(new String[] { "false", "true" }, bs); 855 | 856 | Map state = newMapInitializedWithInstanceKey(); 857 | state.put("a", AggregateString.create("true", InstanceKey.create(98765))); 858 | state.put("b", AggregateString.create("false", InstanceKey.create(98765))); 859 | 860 | Map s = StreamAggregator.sumOfDelta(state, d); 861 | assertEquals("AggregateString => {\"true\":1}", s.get("a").toString()); // same instanceId so count == 1 862 | assertEquals("AggregateString => {\"true\":1}", s.get("b").toString()); 863 | } 864 | 865 | @Test 866 | public void testDeltaBooleanRemove() { 867 | Map mPrevious = newMapInitializedWithInstanceKey(); 868 | mPrevious.put("a", Boolean.TRUE); 869 | mPrevious.put("b", Boolean.FALSE); 870 | 871 | Map mCurrent = newMapInitializedWithInstanceKey(); 872 | 873 | Map d = StreamAggregator.previousAndCurrentToDelta(mPrevious, mCurrent); 874 | String[] as = (String[]) d.get("a"); 875 | String[] bs = (String[]) d.get("b"); 876 | assertArrayEquals(new String[] { "true", null }, as); 877 | assertArrayEquals(new String[] { "false", null }, bs); 878 | 879 | Map state = newMapInitializedWithInstanceKey(); 880 | state.put("a", AggregateString.create("true", InstanceKey.create(98765))); 881 | state.put("b", AggregateString.create("false", InstanceKey.create(98765))); 882 | 883 | Map s = StreamAggregator.sumOfDelta(state, d); 884 | assertEquals("AggregateString => {}", s.get("a").toString()); // same instanceId so count == 1 885 | assertEquals("AggregateString => {}", s.get("b").toString()); 886 | } 887 | 888 | @Test 889 | public void testDeltaNumberListNew() { 890 | Map mCurrent = newMapInitializedWithInstanceKey(); 891 | Map v = new HashMap<>(); 892 | v.put("100", 99); 893 | mCurrent.put("a", v); 894 | 895 | Map d = StreamAggregator.previousAndCurrentToDelta(Collections.emptyMap(), mCurrent); 896 | assertEquals(99l, ((NumberList)d.get("a")).get("100").longValue()); 897 | 898 | Map s = StreamAggregator.sumOfDelta(newMapInitializedWithInstanceKey(), d); 899 | assertEquals(99l, ((NumberList)s.get("a")).get("100").longValue()); 900 | } 901 | 902 | @Test 903 | public void testDeltaNumberList1() { 904 | Map mPrevious = newMapInitializedWithInstanceKey(); 905 | Map v = new HashMap<>(); 906 | v.put("100", 99); 907 | mPrevious.put("a", v); 908 | 909 | Map mCurrent = newMapInitializedWithInstanceKey(); 910 | Map v2 = new HashMap<>(); 911 | v2.put("100", 97); 912 | mCurrent.put("a", v2); 913 | 914 | Map d = StreamAggregator.previousAndCurrentToDelta(mPrevious, mCurrent); 915 | assertEquals(-2l, ((NumberList)d.get("a")).get("100").longValue()); 916 | 917 | Map initial = StreamAggregator.previousAndCurrentToDelta(Collections.emptyMap(), mPrevious); 918 | Map s = StreamAggregator.sumOfDelta(initial, d); 919 | assertEquals(97l, ((NumberList)s.get("a")).get("100").longValue()); 920 | } 921 | 922 | @Test 923 | public void testDeltaNumberList2() { 924 | Map mPrevious = newMapInitializedWithInstanceKey(); 925 | Map v = new HashMap<>(); 926 | v.put("100", 90); 927 | mPrevious.put("a", v); 928 | 929 | Map mCurrent = newMapInitializedWithInstanceKey(); 930 | Map v2 = new HashMap<>(); 931 | v2.put("100", 99); 932 | mCurrent.put("a", v2); 933 | 934 | Map d = StreamAggregator.previousAndCurrentToDelta(mPrevious, mCurrent); 935 | assertEquals(9l, ((NumberList)d.get("a")).get("100").longValue()); 936 | 937 | Map initial = StreamAggregator.previousAndCurrentToDelta(Collections.emptyMap(), mPrevious); 938 | Map s = StreamAggregator.sumOfDelta(initial, d); 939 | assertEquals(99l, ((NumberList)s.get("a")).get("100").longValue()); 940 | } 941 | 942 | @Test 943 | public void testDeltaNumberListRemove() { 944 | Map mPrevious = newMapInitializedWithInstanceKey(); 945 | Map v = new HashMap<>(); 946 | v.put("100", 99); 947 | mPrevious.put("a", v); 948 | 949 | Map d = StreamAggregator.previousAndCurrentToDelta(mPrevious, Collections.emptyMap()); 950 | System.out.println("d: " + d); 951 | assertEquals(-99l, ((NumberList)d.get("a")).get("100").longValue()); 952 | 953 | Map initial = StreamAggregator.previousAndCurrentToDelta(Collections.emptyMap(), mPrevious); 954 | Map s = StreamAggregator.sumOfDelta(initial, d); 955 | assertEquals(0l, ((NumberList)s.get("a")).get("100").longValue()); 956 | } 957 | 958 | } -------------------------------------------------------------------------------- /turbine-core/src/test/java/com/netflix/turbine/internal/RequestCreatorTest.java: -------------------------------------------------------------------------------- 1 | package com.netflix.turbine.internal; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.reactivex.netty.protocol.http.client.HttpClientRequest; 5 | import org.junit.Test; 6 | 7 | import java.net.URI; 8 | import java.util.Base64; 9 | 10 | import static java.nio.charset.StandardCharsets.UTF_8; 11 | import static org.junit.Assert.assertEquals; 12 | import static org.junit.Assert.assertFalse; 13 | 14 | 15 | public class RequestCreatorTest { 16 | 17 | private static final String AUTHORIZATION_HEADER_NAME = "Authorization"; 18 | 19 | @Test 20 | public void doesntAddAuthorizationHeaderWhenNoUserInfoIsDefinedInUri() throws Exception { 21 | // Given 22 | URI uri = new URI("http://myapp.com"); 23 | 24 | // When 25 | HttpClientRequest request = RequestCreator.createRequest(uri); 26 | 27 | // Then 28 | assertFalse(request.getHeaders().contains(AUTHORIZATION_HEADER_NAME)); 29 | } 30 | 31 | @Test 32 | public void addsAuthorizationHeaderWhenUserInfoIsDefinedInUri() throws Exception { 33 | // Given 34 | URI uri = new URI("http://username:password@myapp.com"); 35 | 36 | // When 37 | HttpClientRequest request = RequestCreator.createRequest(uri); 38 | 39 | // Then 40 | assertEquals(basicAuthOf("username", "password"), request.getHeaders().getHeader(AUTHORIZATION_HEADER_NAME)); 41 | } 42 | 43 | @Test 44 | public void removesUserInfoFromUriWhenUserInfoIsDefinedInUri() throws Exception { 45 | // Given 46 | URI uri = new URI("http://username:password@myapp.com"); 47 | 48 | // When 49 | HttpClientRequest request = RequestCreator.createRequest(uri); 50 | 51 | // Then 52 | assertFalse(request.getUri().contains("username:password@")); 53 | } 54 | 55 | private String basicAuthOf(String username, String password) { 56 | return "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes(UTF_8)); 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /turbine-core/src/test/resources/com/netflix/turbine/unsort: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | use List::Util 'shuffle'; 3 | @list = ; 4 | print shuffle(@list); 5 | -------------------------------------------------------------------------------- /turbine-ext/turbine-discovery-eureka1/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'eclipse' 3 | apply plugin: 'com.github.johnrengelman.shadow' 4 | apply plugin: 'maven-publish' 5 | 6 | 7 | sourceCompatibility = JavaVersion.VERSION_1_8 8 | targetCompatibility = JavaVersion.VERSION_1_8 9 | 10 | dependencies { 11 | compile project(":turbine-core") 12 | 13 | compile 'org.slf4j:slf4j-log4j12:1.6.1' 14 | compile 'com.netflix.eureka:eureka-client:1.1.97' 15 | testCompile 'junit:junit-dep:4.10' 16 | testCompile 'org.json:json:20140107' 17 | } 18 | 19 | mainClassName = "com.netflix.turbine.discovery.eureka.StartEurekaTurbine" 20 | 21 | shadowJar { 22 | baseName = 'turbine-discovery-eureka1-executable' 23 | classifier = '' 24 | } 25 | 26 | publishing { 27 | publications { 28 | shadow(MavenPublication) { 29 | from components.shadow 30 | artifactId = 'turbine-discovery-eureka1-executable' 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /turbine-ext/turbine-discovery-eureka1/src/main/java/com/netflix/turbine/discovery/eureka/EurekaInstance.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.discovery.eureka; 17 | 18 | import com.netflix.appinfo.InstanceInfo; 19 | import com.netflix.appinfo.InstanceInfo.InstanceStatus; 20 | 21 | public class EurekaInstance { 22 | 23 | public static enum Status { 24 | UP, DOWN 25 | } 26 | 27 | private final Status status; 28 | private final InstanceInfo instance; 29 | 30 | private EurekaInstance(Status status, InstanceInfo instance) { 31 | this.status = status; 32 | this.instance = instance; 33 | } 34 | 35 | public static EurekaInstance create(InstanceInfo instance) { 36 | Status status; 37 | if (InstanceStatus.UP == instance.getStatus()) { 38 | status = Status.UP; 39 | } else { 40 | status = Status.DOWN; 41 | } 42 | return new EurekaInstance(status, instance); 43 | } 44 | 45 | public static EurekaInstance create(Status status, InstanceInfo instance) { 46 | return new EurekaInstance(status, instance); 47 | } 48 | 49 | public Status getStatus() { 50 | return status; 51 | } 52 | 53 | public InstanceInfo getInstanceInfo() { 54 | return instance; 55 | } 56 | 57 | public String getAppName() { 58 | return instance.getAppName(); 59 | } 60 | 61 | public String getHostName() { 62 | return instance.getHostName(); 63 | } 64 | 65 | public String getIPAddr() { 66 | return instance.getIPAddr(); 67 | } 68 | 69 | public String getVIPAddress() { 70 | return instance.getVIPAddress(); 71 | } 72 | 73 | public String getASGName() { 74 | return instance.getASGName(); 75 | } 76 | 77 | @Override 78 | public int hashCode() { 79 | final int prime = 31; 80 | int result = 1; 81 | result = prime * result + ((getAppName() == null) ? 0 : getAppName().hashCode()); 82 | result = prime * result + ((getASGName() == null) ? 0 : getASGName().hashCode()); 83 | result = prime * result + ((getHostName() == null) ? 0 : getHostName().hashCode()); 84 | result = prime * result + ((getIPAddr() == null) ? 0 : getIPAddr().hashCode()); 85 | result = prime * result + ((getVIPAddress() == null) ? 0 : getVIPAddress().hashCode()); 86 | result = prime * result + ((status == null) ? 0 : status.hashCode()); 87 | return result; 88 | } 89 | 90 | @Override 91 | public boolean equals(Object obj) { 92 | if (this == obj) 93 | return true; 94 | if (obj == null) 95 | return false; 96 | if (getClass() != obj.getClass()) 97 | return false; 98 | EurekaInstance other = (EurekaInstance) obj; 99 | if (getAppName() == null) { 100 | if (other.getAppName() != null) 101 | return false; 102 | } else if (!getAppName().equals(other.getAppName())) 103 | return false; 104 | if (getASGName() == null) { 105 | if (other.getASGName() != null) 106 | return false; 107 | } else if (!getASGName().equals(other.getASGName())) 108 | return false; 109 | if (getHostName() == null) { 110 | if (other.getHostName() != null) 111 | return false; 112 | } else if (!getHostName().equals(other.getHostName())) 113 | return false; 114 | if (getIPAddr() == null) { 115 | if (other.getIPAddr() != null) 116 | return false; 117 | } else if (!getIPAddr().equals(other.getIPAddr())) 118 | return false; 119 | if (getVIPAddress() == null) { 120 | if (other.getVIPAddress() != null) 121 | return false; 122 | } else if (!getVIPAddress().equals(other.getVIPAddress())) 123 | return false; 124 | if (status != other.status) 125 | return false; 126 | 127 | return true; 128 | } 129 | 130 | @Override 131 | public String toString() { 132 | return "EurekaInstance [status=" + status + ", vip=" + instance.getVIPAddress() + ", hostname=" + instance.getHostName() + ", asg=" + instance.getASGName() + "]"; 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /turbine-ext/turbine-discovery-eureka1/src/main/java/com/netflix/turbine/discovery/eureka/EurekaInstanceDiscovery.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Netflix 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.discovery.eureka; 17 | 18 | import java.util.ArrayList; 19 | import java.util.LinkedHashSet; 20 | import java.util.List; 21 | import java.util.Set; 22 | import java.util.concurrent.TimeUnit; 23 | 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | 27 | import rx.Observable; 28 | import rx.Subscriber; 29 | import rx.schedulers.Schedulers; 30 | 31 | import com.netflix.appinfo.InstanceInfo; 32 | import com.netflix.appinfo.MyDataCenterInstanceConfig; 33 | import com.netflix.discovery.DefaultEurekaClientConfig; 34 | import com.netflix.discovery.DiscoveryManager; 35 | import com.netflix.discovery.shared.Application; 36 | import com.netflix.turbine.discovery.eureka.EurekaInstance.Status; 37 | 38 | /** 39 | * Class that encapsulates an {@link InstanceDicovery} implementation that uses Eureka (see https://github.com/Netflix/eureka) 40 | * The plugin requires a list of applications configured. It then queries the set of instances for each application. 41 | * Instance information retrieved from Eureka must be translated to something that Turbine can understand i.e the {@link Instance} class. 42 | * 43 | * All the logic to perform this translation can be overriden here, so that you can provide your own implementation if needed. 44 | */ 45 | public class EurekaInstanceDiscovery { 46 | 47 | private static final Logger logger = LoggerFactory.getLogger(EurekaInstanceDiscovery.class); 48 | 49 | public static void main(String[] args) { 50 | 51 | new EurekaInstanceDiscovery().getInstanceEvents("api").toBlocking().forEach(i -> System.out.println(i)); 52 | } 53 | 54 | public EurekaInstanceDiscovery() { 55 | // initialize eureka client. make sure eureka properties are properly configured in config.properties 56 | DiscoveryManager.getInstance().initComponent(new MyDataCenterInstanceConfig(), new DefaultEurekaClientConfig()); 57 | } 58 | 59 | public Observable getInstanceEvents(String appName) { 60 | return Observable. 61 | create((Subscriber subscriber) -> { 62 | try { 63 | logger.info("Fetching instance list for app: " + appName); 64 | Application app = DiscoveryManager.getInstance().getDiscoveryClient().getApplication(appName); 65 | if (app == null) { 66 | subscriber.onError(new RuntimeException("App not found: " + appName)); 67 | return; 68 | } 69 | List instancesForApp = app.getInstances(); 70 | if (instancesForApp != null) { 71 | logger.info("Received instance list for app: " + appName + " = " + instancesForApp.size()); 72 | for (InstanceInfo instance : instancesForApp) { 73 | if (InstanceInfo.InstanceStatus.UP == instance.getStatus()) { 74 | // we only emit UP instances, the delta process marks DOWN 75 | subscriber.onNext(EurekaInstance.create(instance)); 76 | } 77 | } 78 | subscriber.onCompleted(); 79 | } else { 80 | subscriber.onError(new RuntimeException("Failed to retrieve instances for appName: " + appName)); 81 | } 82 | } catch (Throwable e) { 83 | subscriber.onError(e); 84 | } 85 | }) 86 | .subscribeOn(Schedulers.io()) 87 | .toList() 88 | .repeatWhen(a -> a.flatMap(n -> Observable.timer(30, TimeUnit.SECONDS))) // repeat after 30 second delay 89 | .startWith(new ArrayList()) 90 | .buffer(2, 1) 91 | .filter(l -> l.size() == 2) 92 | .flatMap(EurekaInstanceDiscovery::delta); 93 | } 94 | 95 | static Observable delta(List> listOfLists) { 96 | if (listOfLists.size() == 1) { 97 | return Observable.from(listOfLists.get(0)); 98 | } else { 99 | // diff the two 100 | List newList = listOfLists.get(1); 101 | List oldList = new ArrayList<>(listOfLists.get(0)); 102 | 103 | Set delta = new LinkedHashSet<>(); 104 | delta.addAll(newList); 105 | // remove all that match in old 106 | delta.removeAll(oldList); 107 | 108 | // filter oldList to those that aren't in the newList 109 | oldList.removeAll(newList); 110 | 111 | // for all left in the oldList we'll create DROP events 112 | for (EurekaInstance old : oldList) { 113 | delta.add(EurekaInstance.create(Status.DOWN, old.getInstanceInfo())); 114 | } 115 | 116 | return Observable.from(delta); 117 | } 118 | } 119 | 120 | } -------------------------------------------------------------------------------- /turbine-ext/turbine-discovery-eureka1/src/main/java/com/netflix/turbine/discovery/eureka/EurekaStreamDiscovery.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.discovery.eureka; 17 | 18 | import java.net.URI; 19 | 20 | import com.netflix.turbine.discovery.StreamAction; 21 | import com.netflix.turbine.discovery.StreamDiscovery; 22 | 23 | import rx.Observable; 24 | 25 | public class EurekaStreamDiscovery implements StreamDiscovery { 26 | 27 | public static EurekaStreamDiscovery create(String appName, String uriTemplate) { 28 | return new EurekaStreamDiscovery(appName, uriTemplate); 29 | } 30 | 31 | public final static String HOSTNAME = "{HOSTNAME}"; 32 | private final String uriTemplate; 33 | private final String appName; 34 | 35 | private EurekaStreamDiscovery(String appName, String uriTemplate) { 36 | this.appName = appName; 37 | this.uriTemplate = uriTemplate; 38 | } 39 | 40 | @Override 41 | public Observable getInstanceList() { 42 | return new EurekaInstanceDiscovery() 43 | .getInstanceEvents(appName) 44 | .map(ei -> { 45 | URI uri; 46 | try { 47 | uri = new URI(uriTemplate.replace(HOSTNAME, ei.getHostName())); 48 | } catch (Exception e) { 49 | throw new RuntimeException("Invalid URI", e); 50 | } 51 | if (ei.getStatus() == EurekaInstance.Status.UP) { 52 | return StreamAction.create(StreamAction.ActionType.ADD, uri); 53 | } else { 54 | return StreamAction.create(StreamAction.ActionType.REMOVE, uri); 55 | } 56 | 57 | }); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /turbine-ext/turbine-discovery-eureka1/src/main/java/com/netflix/turbine/discovery/eureka/StartEurekaTurbine.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.discovery.eureka; 17 | 18 | import joptsimple.OptionParser; 19 | import joptsimple.OptionSet; 20 | 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | import com.netflix.turbine.Turbine; 25 | 26 | public class StartEurekaTurbine { 27 | private static final Logger logger = LoggerFactory.getLogger(StartEurekaTurbine.class); 28 | 29 | public static void main(String[] args) { 30 | OptionParser optionParser = new OptionParser(); 31 | optionParser.accepts("port").withRequiredArg(); 32 | optionParser.accepts("app").withRequiredArg(); 33 | optionParser.accepts("urlTemplate").withRequiredArg(); 34 | 35 | OptionSet options = optionParser.parse(args); 36 | int port = -1; 37 | if (!options.has("port")) { 38 | System.err.println("Argument -port required for SSE HTTP server to start on. Eg. -port 8888"); 39 | System.exit(-1); 40 | } else { 41 | try { 42 | port = Integer.parseInt(String.valueOf(options.valueOf("port"))); 43 | } catch (NumberFormatException e) { 44 | System.err.println("Value of port must be an integer but was: " + options.valueOf("port")); 45 | } 46 | } 47 | 48 | String app = null; 49 | if (!options.has("app")) { 50 | System.err.println("Argument -app required for Eureka instance discovery. Eg. -app api"); 51 | System.exit(-1); 52 | } else { 53 | app = String.valueOf(options.valueOf("app")); 54 | } 55 | 56 | String template = null; 57 | if (!options.has("urlTemplate")) { 58 | System.err.println("Argument -urlTemplate required. Eg. http://" + EurekaStreamDiscovery.HOSTNAME + "/metrics.stream"); 59 | System.exit(-1); 60 | } else { 61 | template = String.valueOf(options.valueOf("urlTemplate")); 62 | if (!template.contains(EurekaStreamDiscovery.HOSTNAME)) { 63 | System.err.println("Argument -urlTemplate must contain " + EurekaStreamDiscovery.HOSTNAME + " marker. Eg. http://" + EurekaStreamDiscovery.HOSTNAME + "/metrics.stream"); 64 | System.exit(-1); 65 | } 66 | } 67 | 68 | logger.info("Turbine => Eureka App: " + app); 69 | logger.info("Turbine => Eureka URL Template: " + template); 70 | 71 | try { 72 | Turbine.startServerSentEventServer(port, EurekaStreamDiscovery.create(app, template)); 73 | } catch (Throwable e) { 74 | e.printStackTrace(); 75 | } 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /turbine-ext/turbine-discovery-eureka1/src/main/resources/eureka-client.properties: -------------------------------------------------------------------------------- 1 | # Eureka Client configuration for your service. These properties are used by eureka to contact the eureka server 2 | # for all eureka operations. 3 | # Properties based configuration for eureka client. The properties specified here is mostly what the users 4 | # need to change. All of these can be specified as a java system property with -D option (eg)-Deureka.region=us-east-1 5 | # Refer to eureka wiki (https://github.com/Netflix/eureka/wiki/Configuring-Eureka) for details about these configurations. 6 | 7 | #Region where eureka is deployed -For AWS specify one of the AWS regions, for other datacenters specify a arbitrary string 8 | #indicating the region.This is normally specified as a -D option (eg) -Deureka.region=us-east-1 9 | eureka.region=us-east-1 10 | 11 | #The port where the service will be running and serving requests 12 | eureka.port=7001 13 | 14 | #For eureka clients running in eureka server, it needs to connect to servers in other zones 15 | eureka.preferSameZone=false 16 | 17 | eureka.shouldUseDns=true 18 | eureka.domainName=your.hostname.here 19 | eureka.eurekaServer.context=/discovery/v2 20 | eureka.us-east-1.availabilityZones=us-east-1c,us-east-1d,us-east-1e 21 | -------------------------------------------------------------------------------- /turbine-ext/turbine-discovery-eureka1/src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # Root logger option 2 | log4j.rootLogger=INFO, stdout 3 | 4 | # Direct log messages to stdout 5 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 6 | log4j.appender.stdout.Target=System.out 7 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 8 | log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n -------------------------------------------------------------------------------- /turbine-ext/turbine-discovery-eureka1/src/test/java/com/netflix/turbine/discovery/eureka/EurekaInstanceDiscoveryTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.discovery.eureka; 17 | 18 | import java.util.ArrayList; 19 | import java.util.Arrays; 20 | import java.util.List; 21 | 22 | import org.junit.Test; 23 | 24 | import rx.Observable; 25 | import rx.observers.TestSubscriber; 26 | 27 | import com.netflix.appinfo.InstanceInfo; 28 | import com.netflix.appinfo.InstanceInfo.InstanceStatus; 29 | import com.netflix.turbine.discovery.eureka.EurekaInstance; 30 | import com.netflix.turbine.discovery.eureka.EurekaInstanceDiscovery; 31 | import com.netflix.turbine.discovery.eureka.EurekaInstance.Status; 32 | 33 | public class EurekaInstanceDiscoveryTest { 34 | 35 | @Test 36 | public void testDeltaRemoveDuplicateAddSecond() { 37 | EurekaInstance a = EurekaInstance.create(InstanceInfo.Builder.newBuilder() 38 | .setAppName("api").setHostName("hostname1").setStatus(InstanceStatus.UP).build()); 39 | Observable> first = Observable.just(a).toList(); 40 | 41 | EurekaInstance b = EurekaInstance.create(InstanceInfo.Builder.newBuilder() 42 | .setAppName("api").setHostName("hostname1").setStatus(InstanceStatus.DOWN).build()); 43 | Observable> second = Observable.just(a, b).toList(); 44 | 45 | TestSubscriber ts = new TestSubscriber(); 46 | 47 | Observable 48 | .just(first, second) 49 | .flatMap(o -> o) 50 | .startWith(new ArrayList()) 51 | .buffer(2, 1) 52 | .filter(l -> l.size() == 2) 53 | .flatMap(EurekaInstanceDiscovery::delta) 54 | .subscribe(ts); 55 | 56 | ts.assertReceivedOnNext(Arrays.asList(a, b)); 57 | } 58 | 59 | @Test 60 | public void testDrop() { 61 | EurekaInstance a = EurekaInstance.create(InstanceInfo.Builder.newBuilder() 62 | .setAppName("api").setHostName("hostname1").setStatus(InstanceStatus.UP).build()); 63 | Observable> first = Observable.just(a).toList(); 64 | 65 | EurekaInstance b = EurekaInstance.create(InstanceInfo.Builder.newBuilder() 66 | .setAppName("api").setHostName("hostname1").setStatus(InstanceStatus.DOWN).build()); 67 | Observable> second = Observable.just(b).toList(); 68 | 69 | TestSubscriber ts = new TestSubscriber(); 70 | 71 | Observable 72 | .just(first, second) 73 | .flatMap(o -> o) 74 | .startWith(new ArrayList()) 75 | .buffer(2, 1) 76 | .filter(l -> l.size() == 2) 77 | .flatMap(EurekaInstanceDiscovery::delta) 78 | .subscribe(ts); 79 | 80 | ts.assertReceivedOnNext(Arrays.asList(a, b)); 81 | } 82 | 83 | @Test 84 | public void testAddRemoveAddRemove() { 85 | // start with 4 86 | EurekaInstance a1 = EurekaInstance.create(InstanceInfo.Builder.newBuilder() 87 | .setAppName("api").setHostName("hostname1").setStatus(InstanceStatus.UP).build()); 88 | EurekaInstance a2 = EurekaInstance.create(InstanceInfo.Builder.newBuilder() 89 | .setAppName("api").setHostName("hostname2").setStatus(InstanceStatus.UP).build()); 90 | EurekaInstance a3 = EurekaInstance.create(InstanceInfo.Builder.newBuilder() 91 | .setAppName("api").setHostName("hostname3").setStatus(InstanceStatus.UP).build()); 92 | EurekaInstance a4 = EurekaInstance.create(InstanceInfo.Builder.newBuilder() 93 | .setAppName("api").setHostName("hostname4").setStatus(InstanceStatus.UP).build()); 94 | Observable> first = Observable.just(a1, a2, a3, a4).toList(); 95 | 96 | // mark one of them as DOWN 97 | EurekaInstance b4 = EurekaInstance.create(InstanceInfo.Builder.newBuilder() 98 | .setAppName("api").setHostName("hostname4").setStatus(InstanceStatus.DOWN).build()); 99 | Observable> second = Observable.just(a1, a2, a3, b4).toList(); 100 | 101 | // then completely drop 2 of them 102 | Observable> third = Observable.just(a1, a2).toList(); 103 | 104 | TestSubscriber ts = new TestSubscriber(); 105 | 106 | Observable 107 | .just(first, second, third) 108 | .flatMap(o -> o) 109 | .startWith(new ArrayList()) 110 | .buffer(2, 1) 111 | .filter(l -> l.size() == 2) 112 | .flatMap(EurekaInstanceDiscovery::delta) 113 | .subscribe(ts); 114 | 115 | // expected ... 116 | // UP a1, UP a2, UP a3, UP a4 117 | // DOWN b4 118 | // DOWN a3 119 | ts.assertReceivedOnNext(Arrays.asList(a1, a2, a3, a4, b4, EurekaInstance.create(Status.DOWN, a3.getInstanceInfo()), b4)); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /turbine-ext/turbine-discovery-eureka2/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'eclipse' 3 | apply plugin: 'com.github.johnrengelman.shadow' 4 | apply plugin: 'maven-publish' 5 | 6 | 7 | sourceCompatibility = JavaVersion.VERSION_1_8 8 | targetCompatibility = JavaVersion.VERSION_1_8 9 | 10 | dependencies { 11 | compile project(":turbine-core") 12 | 13 | compile 'org.slf4j:slf4j-log4j12:1.6.1' 14 | compile 'com.netflix.eureka2:eureka-client:2.0.0-DP2' 15 | testCompile 'junit:junit-dep:4.10' 16 | testCompile 'org.json:json:20140107' 17 | } 18 | 19 | mainClassName = "com.netflix.turbine.discovery.eureka.StartEurekaTurbine" 20 | 21 | shadowJar { 22 | baseName = 'turbine-discovery-eureka2-executable' 23 | classifier = '' 24 | } 25 | 26 | publishing { 27 | publications { 28 | shadow(MavenPublication) { 29 | from components.shadow 30 | artifactId = 'turbine-discovery-eureka2-executable' 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /turbine-ext/turbine-discovery-eureka2/src/main/java/com/netflix/turbine/discovery/eureka/EurekaInstance.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.discovery.eureka; 17 | 18 | import com.netflix.eureka2.interests.ChangeNotification; 19 | import com.netflix.eureka2.registry.InstanceInfo; 20 | import com.netflix.eureka2.registry.ServicePort; 21 | import com.netflix.eureka2.registry.NetworkAddress.ProtocolType; 22 | 23 | import java.util.HashMap; 24 | import java.util.HashSet; 25 | import java.util.Map; 26 | import java.util.stream.Collectors; 27 | 28 | public class EurekaInstance { 29 | 30 | public static enum Status { 31 | UP, DOWN 32 | } 33 | 34 | private final String cluster; 35 | private final Status status; 36 | private final String hostname; 37 | private final int port; 38 | private final Map attributes; 39 | 40 | private EurekaInstance(String cluster, Status status, String hostname, int port) { 41 | this.cluster = cluster; 42 | this.status = status; 43 | this.hostname = hostname; 44 | this.port = port; 45 | this.attributes = new HashMap<>(); 46 | } 47 | 48 | public static EurekaInstance from(ChangeNotification notification) { 49 | InstanceInfo instanceInfo = notification.getData(); 50 | String cluster = instanceInfo.getApp(); 51 | 52 | String ipAddress = instanceInfo.getDataCenterInfo() 53 | .getAddresses().stream() 54 | .filter(na -> na.getProtocolType() == ProtocolType.IPv4) 55 | .collect(Collectors.toList()).get(0).getIpAddress(); 56 | HashSet servicePorts = instanceInfo.getPorts(); 57 | int port = instanceInfo.getPorts().iterator().next().getPort(); 58 | 59 | Status status = ChangeNotification.Kind.Delete == notification.getKind() 60 | ? Status.DOWN // count deleted as DOWN 61 | : (InstanceInfo.Status.UP == instanceInfo.getStatus() ? Status.UP : Status.DOWN); 62 | 63 | return new EurekaInstance(cluster, status, ipAddress, port); 64 | } 65 | 66 | public Status getStatus() { 67 | return status; 68 | } 69 | 70 | public String getCluster() { 71 | return cluster; 72 | } 73 | 74 | public String getHost() { 75 | return hostname; 76 | } 77 | 78 | public boolean isUp() { 79 | return Status.UP == status; 80 | } 81 | 82 | public int getPort() { 83 | return port; 84 | } 85 | 86 | @Override 87 | public boolean equals(Object o) { 88 | if (this == o) return true; 89 | if (!(o instanceof EurekaInstance)) return false; 90 | 91 | EurekaInstance that = (EurekaInstance) o; 92 | 93 | if (port != that.port) return false; 94 | if (attributes != null ? !attributes.equals(that.attributes) : that.attributes != null) return false; 95 | if (cluster != null ? !cluster.equals(that.cluster) : that.cluster != null) return false; 96 | if (hostname != null ? !hostname.equals(that.hostname) : that.hostname != null) return false; 97 | if (status != that.status) return false; 98 | 99 | return true; 100 | } 101 | 102 | @Override 103 | public int hashCode() { 104 | int result = cluster != null ? cluster.hashCode() : 0; 105 | result = 31 * result + (status != null ? status.hashCode() : 0); 106 | result = 31 * result + (hostname != null ? hostname.hashCode() : 0); 107 | result = 31 * result + port; 108 | result = 31 * result + (attributes != null ? attributes.hashCode() : 0); 109 | return result; 110 | } 111 | 112 | @Override 113 | public String toString() { 114 | return "EurekaInstance{" + 115 | "cluster='" + cluster + '\'' + 116 | ", status=" + status + 117 | ", hostname='" + hostname + '\'' + 118 | ", port=" + port + 119 | ", attributes=" + attributes + 120 | '}'; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /turbine-ext/turbine-discovery-eureka2/src/main/java/com/netflix/turbine/discovery/eureka/EurekaInstanceDiscovery.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Netflix 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.discovery.eureka; 17 | 18 | import com.netflix.eureka2.client.Eureka; 19 | import com.netflix.eureka2.client.EurekaClient; 20 | import com.netflix.eureka2.client.resolver.ServerResolvers; 21 | import com.netflix.eureka2.interests.ChangeNotification; 22 | import com.netflix.eureka2.registry.InstanceInfo; 23 | import joptsimple.OptionParser; 24 | import joptsimple.OptionSet; 25 | import org.slf4j.Logger; 26 | import org.slf4j.LoggerFactory; 27 | 28 | import rx.Observable; 29 | import rx.functions.Action1; 30 | import rx.functions.Func1; 31 | 32 | /** 33 | * Class that encapsulates an {@link InstanceDicovery} implementation that uses Eureka (see https://github.com/Netflix/eureka) 34 | * The plugin requires a list of applications configured. It then queries the set of instances for each application. 35 | * Instance information retrieved from Eureka must be translated to something that Turbine can understand i.e the 36 | * {@link EurekaInstance} class. 37 | * 38 | * All the logic to perform this translation can be overridden here, so that you can provide your own implementation if needed. 39 | */ 40 | public class EurekaInstanceDiscovery { 41 | 42 | private static final Logger logger = LoggerFactory.getLogger(EurekaInstanceDiscovery.class); 43 | 44 | private final EurekaClient eurekaClient; 45 | 46 | public static void main(String[] args) { 47 | OptionParser optionParser = new OptionParser(); 48 | optionParser.accepts("eurekaPort").withRequiredArg(); 49 | optionParser.accepts("eurekaHostname").withRequiredArg(); 50 | optionParser.accepts("app").withRequiredArg(); 51 | 52 | OptionSet options = optionParser.parse(args); 53 | 54 | int eurekaPort = -1; 55 | if (!options.has("eurekaPort")) { 56 | System.err.println("Argument --eurekaPort required: port of eurekaServer"); 57 | System.exit(-1); 58 | } else { 59 | try { 60 | eurekaPort = Integer.parseInt(String.valueOf(options.valueOf("eurekaPort"))); 61 | } catch (NumberFormatException e) { 62 | System.err.println("Value of eurekaPort must be an integer but was: " + options.valueOf("eurekaPort")); 63 | } 64 | } 65 | 66 | String eurekaHostname = null; 67 | if (!options.has("eurekaHostname")) { 68 | System.err.println("Argument --eurekaHostname required: hostname of eurekaServer"); 69 | System.exit(-1); 70 | } else { 71 | eurekaHostname = String.valueOf(options.valueOf("eurekaHostname")); 72 | } 73 | 74 | String app = null; 75 | if (!options.has("app")) { 76 | System.err.println("Argument --app required for Eureka instance discovery. Eg. -app api"); 77 | System.exit(-1); 78 | } else { 79 | app = String.valueOf(options.valueOf("app")); 80 | } 81 | 82 | EurekaClient eurekaClient = Eureka.newClient(ServerResolvers.just(eurekaHostname, eurekaPort), null); 83 | new EurekaInstanceDiscovery(eurekaClient) 84 | .getInstanceEvents(app).toBlocking().forEach(i -> System.out.println(i)); 85 | } 86 | 87 | public EurekaInstanceDiscovery(EurekaClient eurekaClient) { 88 | this.eurekaClient = eurekaClient; 89 | } 90 | 91 | public Observable getInstanceEvents(String appName) { 92 | return eurekaClient.forApplication(appName) 93 | .map(new Func1, EurekaInstance>() { 94 | @Override 95 | public EurekaInstance call(ChangeNotification notification) { 96 | try { 97 | return EurekaInstance.from(notification); 98 | } catch (Exception e) { 99 | logger.warn("Error parsing notification from eurekaClient {}", notification); 100 | } 101 | return null; 102 | } 103 | }).filter(new Func1() { 104 | @Override 105 | public Boolean call(EurekaInstance eurekaInstance) { 106 | return eurekaInstance != null; 107 | } 108 | }); 109 | } 110 | } -------------------------------------------------------------------------------- /turbine-ext/turbine-discovery-eureka2/src/main/java/com/netflix/turbine/discovery/eureka/EurekaStreamDiscovery.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.discovery.eureka; 17 | 18 | import java.net.URI; 19 | 20 | import com.netflix.eureka2.client.EurekaClient; 21 | import com.netflix.turbine.discovery.StreamAction; 22 | import com.netflix.turbine.discovery.StreamDiscovery; 23 | 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | import rx.Observable; 27 | 28 | public class EurekaStreamDiscovery implements StreamDiscovery { 29 | 30 | private static final Logger logger = LoggerFactory.getLogger(EurekaStreamDiscovery.class); 31 | 32 | public static EurekaStreamDiscovery create(String appName, String uriTemplate, EurekaClient eurekaClient) { 33 | return new EurekaStreamDiscovery(appName, uriTemplate, eurekaClient); 34 | } 35 | 36 | public final static String HOSTNAME = "{HOSTNAME}"; 37 | private final String uriTemplate; 38 | private final String appName; 39 | private final EurekaClient eurekaClient; 40 | 41 | private EurekaStreamDiscovery(String appName, String uriTemplate, EurekaClient eurekaClient) { 42 | this.appName = appName; 43 | this.uriTemplate = uriTemplate; 44 | this.eurekaClient = eurekaClient; 45 | } 46 | 47 | @Override 48 | public Observable getInstanceList() { 49 | return new EurekaInstanceDiscovery(eurekaClient) 50 | .getInstanceEvents(appName) 51 | .map(ei -> { 52 | URI uri; 53 | String uriString = uriTemplate.replace(HOSTNAME, ei.getHost() + ":" + ei.getPort()); 54 | try { 55 | uri = new URI(uriString); 56 | } catch (Exception e) { 57 | throw new RuntimeException("Invalid URI: " + uriString, e); 58 | } 59 | if (ei.getStatus() == EurekaInstance.Status.UP) { 60 | logger.info("StreamAction ADD"); 61 | return StreamAction.create(StreamAction.ActionType.ADD, uri); 62 | } else { 63 | logger.info("StreamAction REMOVE"); 64 | return StreamAction.create(StreamAction.ActionType.REMOVE, uri); 65 | } 66 | 67 | }); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /turbine-ext/turbine-discovery-eureka2/src/main/java/com/netflix/turbine/discovery/eureka/StartEurekaTurbine.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.discovery.eureka; 17 | 18 | import com.netflix.eureka2.client.Eureka; 19 | import com.netflix.eureka2.client.EurekaClient; 20 | import com.netflix.eureka2.client.resolver.ServerResolvers; 21 | import joptsimple.OptionParser; 22 | import joptsimple.OptionSet; 23 | 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | 27 | import com.netflix.turbine.Turbine; 28 | 29 | public class StartEurekaTurbine { 30 | private static final Logger logger = LoggerFactory.getLogger(StartEurekaTurbine.class); 31 | 32 | public static void main(String[] args) { 33 | OptionParser optionParser = new OptionParser(); 34 | optionParser.accepts("port").withRequiredArg(); 35 | optionParser.accepts("app").withRequiredArg(); 36 | optionParser.accepts("urlTemplate").withRequiredArg(); 37 | optionParser.accepts("eurekaPort").withRequiredArg(); 38 | optionParser.accepts("eurekaHostname").withRequiredArg(); 39 | 40 | OptionSet options = optionParser.parse(args); 41 | 42 | int port = -1; 43 | if (!options.has("port")) { 44 | System.err.println("Argument -port required for SSE HTTP server to start on. Eg. -port 8888"); 45 | System.exit(-1); 46 | } else { 47 | try { 48 | port = Integer.parseInt(String.valueOf(options.valueOf("port"))); 49 | } catch (NumberFormatException e) { 50 | System.err.println("Value of port must be an integer but was: " + options.valueOf("port")); 51 | } 52 | } 53 | 54 | String app = null; 55 | if (!options.has("app")) { 56 | System.err.println("Argument -app required for Eureka instance discovery. Eg. -app api"); 57 | System.exit(-1); 58 | } else { 59 | app = String.valueOf(options.valueOf("app")); 60 | } 61 | 62 | String template = null; 63 | if (!options.has("urlTemplate")) { 64 | System.err.println("Argument -urlTemplate required. Eg. http://" + EurekaStreamDiscovery.HOSTNAME + "/metrics.stream"); 65 | System.exit(-1); 66 | } else { 67 | template = String.valueOf(options.valueOf("urlTemplate")); 68 | if (!template.contains(EurekaStreamDiscovery.HOSTNAME)) { 69 | System.err.println("Argument -urlTemplate must contain " + EurekaStreamDiscovery.HOSTNAME + " marker. Eg. http://" + EurekaStreamDiscovery.HOSTNAME + "/metrics.stream"); 70 | System.exit(-1); 71 | } 72 | } 73 | 74 | // 75 | // Eureka2 Configs 76 | // 77 | int eurekaPort = -1; 78 | if (!options.has("eurekaPort")) { 79 | System.err.println("Argument --eurekaPort required: port of eurekaServer"); 80 | System.exit(-1); 81 | } else { 82 | try { 83 | eurekaPort = Integer.parseInt(String.valueOf(options.valueOf("eurekaPort"))); 84 | } catch (NumberFormatException e) { 85 | System.err.println("Value of eurekaPort must be an integer but was: " + options.valueOf("eurekaPort")); 86 | } 87 | } 88 | 89 | String eurekaHostname = null; 90 | if (!options.has("eurekaHostname")) { 91 | System.err.println("Argument --eurekaHostname required: hostname of eurekaServer"); 92 | System.exit(-1); 93 | } else { 94 | eurekaHostname = String.valueOf(options.valueOf("eurekaHostname")); 95 | } 96 | 97 | logger.info("Turbine => Eureka App: " + app); 98 | logger.info("Turbine => Eureka URL Template: " + template); 99 | 100 | try { 101 | EurekaClient eurekaClient = Eureka.newClient(ServerResolvers.just(eurekaHostname, eurekaPort), null); 102 | Turbine.startServerSentEventServer(port, EurekaStreamDiscovery.create(app, template, eurekaClient)); 103 | } catch (Throwable e) { 104 | e.printStackTrace(); 105 | } 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /turbine-ext/turbine-discovery-eureka2/src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # Root logger option 2 | log4j.rootLogger=INFO, stdout 3 | 4 | # Direct log messages to stdout 5 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 6 | log4j.appender.stdout.Target=System.out 7 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 8 | log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n -------------------------------------------------------------------------------- /turbine-ext/turbine-discovery-eureka2/src/test/java/com/netflix/turbine/discovery/eureka/EurekaInstanceDiscoveryTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.netflix.turbine.discovery.eureka; 17 | 18 | import java.util.Arrays; 19 | import java.util.Queue; 20 | import java.util.concurrent.ConcurrentLinkedQueue; 21 | 22 | import com.google.common.collect.Sets; 23 | import com.netflix.eureka2.client.EurekaClient; 24 | import com.netflix.eureka2.interests.ChangeNotification; 25 | import com.netflix.eureka2.interests.Interest; 26 | import com.netflix.eureka2.interests.Interests; 27 | import com.netflix.eureka2.registry.DataCenterInfo; 28 | import com.netflix.eureka2.registry.InstanceInfo; 29 | import com.netflix.eureka2.registry.NetworkAddress; 30 | import com.netflix.eureka2.registry.ServicePort; 31 | import com.netflix.eureka2.registry.datacenter.BasicDataCenterInfo; 32 | import junit.framework.Assert; 33 | import org.junit.After; 34 | import org.junit.Before; 35 | import org.junit.Test; 36 | 37 | import rx.Observable; 38 | import rx.observers.TestSubscriber; 39 | 40 | public class EurekaInstanceDiscoveryTest { 41 | 42 | private InstanceInfo upInstanceInfo1; 43 | private InstanceInfo downInstanceInfo1; 44 | private InstanceInfo upInstanceInfo2; 45 | 46 | private EurekaClient testEurekaClient; 47 | private EurekaInstanceDiscovery eurekaInstanceDiscovery; 48 | 49 | @Before 50 | public void setUp() { 51 | NetworkAddress defaultAddress = NetworkAddress.NetworkAddressBuilder.aNetworkAddress() 52 | .withLabel("public") 53 | .withHostName("hostname") 54 | .withProtocolType(NetworkAddress.ProtocolType.IPv4) 55 | .build(); 56 | 57 | DataCenterInfo defaultDataCenterInfo = new BasicDataCenterInfo.Builder() 58 | .withName("default") 59 | .withAddresses(defaultAddress) 60 | .build(); 61 | 62 | this.upInstanceInfo1 = new InstanceInfo.Builder() 63 | .withId("id-1") 64 | .withApp("API") 65 | .withStatus(InstanceInfo.Status.UP) 66 | .withDataCenterInfo(defaultDataCenterInfo) 67 | .withPorts(Sets.newHashSet(new ServicePort(8080, false))) 68 | .build(); 69 | 70 | this.downInstanceInfo1 = new InstanceInfo.Builder() 71 | .withId("id-1") 72 | .withApp("API") 73 | .withStatus(InstanceInfo.Status.DOWN) 74 | .withDataCenterInfo(defaultDataCenterInfo) 75 | .withPorts(Sets.newHashSet(new ServicePort(8080, false))) 76 | .build(); 77 | 78 | this.upInstanceInfo2 = new InstanceInfo.Builder() 79 | .withId("id-2") 80 | .withApp("API") 81 | .withStatus(InstanceInfo.Status.UP) 82 | .withDataCenterInfo(defaultDataCenterInfo) 83 | .withPorts(Sets.newHashSet(new ServicePort(8080, false))) 84 | .build(); 85 | 86 | testEurekaClient = new TestEurekaClient(); 87 | eurekaInstanceDiscovery = new EurekaInstanceDiscovery(testEurekaClient); 88 | } 89 | 90 | @After 91 | public void tearDown() { 92 | testEurekaClient.close(); 93 | } 94 | 95 | @Test 96 | public void testUpdateToSameInstance() { 97 | EurekaInstance a = EurekaInstance.from(new ChangeNotification<>(ChangeNotification.Kind.Add, upInstanceInfo1)); 98 | EurekaInstance b = EurekaInstance.from(new ChangeNotification<>(ChangeNotification.Kind.Add, downInstanceInfo1)); 99 | 100 | Assert.assertEquals(EurekaInstance.Status.UP, a.getStatus()); 101 | Assert.assertEquals(EurekaInstance.Status.DOWN, b.getStatus()); 102 | 103 | TestSubscriber ts = new TestSubscriber<>(); 104 | 105 | testEurekaClient.register(upInstanceInfo1); 106 | testEurekaClient.update(downInstanceInfo1); 107 | 108 | eurekaInstanceDiscovery.getInstanceEvents("API").subscribe(ts); 109 | 110 | ts.assertReceivedOnNext(Arrays.asList(a, b)); 111 | } 112 | 113 | @Test 114 | public void testInstanceRegisterUnregister() { 115 | EurekaInstance a = EurekaInstance.from(new ChangeNotification<>(ChangeNotification.Kind.Add, upInstanceInfo1)); 116 | EurekaInstance b = EurekaInstance.from(new ChangeNotification<>(ChangeNotification.Kind.Delete, upInstanceInfo1)); 117 | 118 | Assert.assertEquals(EurekaInstance.Status.UP, a.getStatus()); 119 | Assert.assertEquals(EurekaInstance.Status.DOWN, b.getStatus()); 120 | 121 | TestSubscriber ts = new TestSubscriber<>(); 122 | 123 | testEurekaClient.register(upInstanceInfo1); 124 | testEurekaClient.unregister(upInstanceInfo1); 125 | 126 | eurekaInstanceDiscovery.getInstanceEvents("API").subscribe(ts); 127 | 128 | ts.assertReceivedOnNext(Arrays.asList(a, b)); 129 | } 130 | 131 | @Test 132 | public void testMultipleInstances() { 133 | EurekaInstance a = EurekaInstance.from(new ChangeNotification<>(ChangeNotification.Kind.Add, upInstanceInfo1)); 134 | EurekaInstance b = EurekaInstance.from(new ChangeNotification<>(ChangeNotification.Kind.Delete, upInstanceInfo1)); 135 | EurekaInstance c = EurekaInstance.from(new ChangeNotification<>(ChangeNotification.Kind.Add, upInstanceInfo2)); 136 | 137 | Assert.assertEquals(EurekaInstance.Status.UP, a.getStatus()); 138 | Assert.assertEquals(EurekaInstance.Status.DOWN, b.getStatus()); 139 | Assert.assertEquals(EurekaInstance.Status.UP, c.getStatus()); 140 | 141 | TestSubscriber ts = new TestSubscriber<>(); 142 | 143 | testEurekaClient.register(upInstanceInfo1); 144 | testEurekaClient.unregister(upInstanceInfo1); 145 | testEurekaClient.register(upInstanceInfo2); 146 | 147 | eurekaInstanceDiscovery.getInstanceEvents("API").subscribe(ts); 148 | 149 | ts.assertReceivedOnNext(Arrays.asList(a, b, c)); 150 | } 151 | 152 | 153 | private static class TestEurekaClient extends EurekaClient { 154 | private Queue> messages; 155 | 156 | TestEurekaClient() { 157 | this.messages = new ConcurrentLinkedQueue<>(); 158 | } 159 | 160 | @Override 161 | public Observable register(InstanceInfo instanceInfo) { 162 | messages.add(new ChangeNotification<>(ChangeNotification.Kind.Add, instanceInfo)); 163 | return Observable.empty(); 164 | } 165 | 166 | @Override 167 | public Observable update(InstanceInfo instanceInfo) { 168 | messages.add(new ChangeNotification<>(ChangeNotification.Kind.Modify, instanceInfo)); 169 | return Observable.empty(); 170 | } 171 | 172 | @Override 173 | public Observable unregister(InstanceInfo instanceInfo) { 174 | messages.add(new ChangeNotification<>(ChangeNotification.Kind.Delete, instanceInfo)); 175 | return Observable.empty(); 176 | } 177 | 178 | @Override 179 | public Observable> forInterest(Interest interest) { 180 | return Observable.from(messages); 181 | } 182 | 183 | @Override 184 | public Observable> forApplication(String s) { 185 | return forInterest(Interests.forApplications(s)); 186 | } 187 | 188 | @Override 189 | public Observable> forVips(String... strings) { 190 | throw new RuntimeException("Not Implemented"); 191 | } 192 | 193 | @Override 194 | public void close() { 195 | messages.clear(); 196 | } 197 | } 198 | } 199 | --------------------------------------------------------------------------------