├── .gitignore ├── LICENSE ├── README.md ├── findbugs-exclude.xml ├── generate-cert.sh ├── pom.xml └── src └── main ├── java └── com │ └── codahale │ └── grpcproxy │ ├── GreeterService.java │ ├── HelloWorldClient.java │ ├── HelloWorldServer.java │ ├── LegacyHttpServer.java │ ├── ProxyHandlerRegistry.java │ ├── ProxyRpcServer.java │ ├── Runner.java │ ├── stats │ ├── IntervalAdder.java │ ├── IntervalCount.java │ ├── Recorder.java │ └── Snapshot.java │ └── util │ ├── ByteArrayMarshaller.java │ ├── Netty.java │ ├── PrettyPrintingDecorator.java │ ├── StatsTracerFactory.java │ └── TlsContext.java ├── proto └── helloworld.proto └── resources └── logback.xml /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | dependency-reduced-pom.xml 8 | buildNumber.properties 9 | .mvn/timing.properties 10 | cert.* 11 | -------------------------------------------------------------------------------- /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 2017 Coda Hale 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 | # gprc-proxy 2 | 3 | An experiment in writing a gRPC proxy frontend for HTTP/protobuf services. 4 | 5 | ## No. Why. 6 | 7 | Sometimes things are written in languages which aren't well-suited to running complex, long-lived 8 | services like gRPC. Sometimes things are running in web servers and that already works really well. 9 | Sometimes those things do important work that other stuff in other platforms needs to use. 10 | 11 | As for gRPC, it's the best RPC framework because it's a thin layer on top of HTTP2, which has lovely 12 | multiplexing, etc. etc. all in a totally standard protocol. 13 | 14 | ## Ok. How. 15 | 16 | This project has three moving parts: 17 | 18 | 1. An HTTP/1.1 server (running on Jetty) which implements a Protobuf-based hello world service. 19 | Pretending to be a bunch of well-tested, battle-hardened business logic trapped in a doofy 20 | runtime. 21 | 2. A gRPC client which implements the client-side of that service, but not over HTTP/1.1. Just a 22 | bog-standard gRPC client. How ever will it talk to the HTTP/1.1 server? 23 | 3. Our hero, a proxying gRPC server. When a request comes in, it reads the Protobuf message without 24 | attempting to decode it, proxies that to the backend (passing the gRPC service/method name as 25 | a query parameter named `method`) in a `POST` request, reads the Protobuf response from the 26 | backend server without parsing it, and proxies the response back to the gRPC client. 27 | Surprisingly, this works. 28 | 29 | ## What's it use? 30 | 31 | * gRPC 1.3.0 32 | * OkHttp 33 | * mutual TLS via OpenSSL (on macOS, run `brew install openssl apr`) 34 | 35 | ## Should I use this? 36 | 37 | Not in its current form, hell no. This is just a proof-of-concept. 38 | 39 | ## Does it get better? 40 | 41 | Yes. OkHttp is HTTP2-compatible, so if your weird PHP service is behind Nginx, you get free 42 | connection management, multiplexing, etc. 43 | 44 | ## License 45 | 46 | Copyright © 2017 Coda Hale 47 | 48 | Distributed under the Apache License 2.0. 49 | -------------------------------------------------------------------------------- /findbugs-exclude.xml: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /generate-cert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Based on a script which was written by Martijn Vermaat 3 | set -euo pipefail 4 | IFS=$'\n\t' 5 | 6 | if [ $# -lt 1 ]; then 7 | echo "Usage: $0 " 8 | exit 1 9 | fi 10 | 11 | common_name="$1" 12 | args="${@:2}" 13 | config="$(mktemp)" 14 | 15 | dnss= 16 | ips= 17 | for arg in ${args}; do 18 | if [[ "${arg}" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then 19 | ips+="${arg} " 20 | else 21 | dnss+="${arg} " 22 | fi 23 | done 24 | 25 | altnames= 26 | subjectaltline= 27 | 28 | i=0 29 | for dns in ${dnss}; do 30 | i=$(($i+1)) 31 | altnames+="DNS.${i} = ${dns}"$'\n' 32 | subjectaltline="subjectAltName = @alt_names" 33 | done 34 | 35 | i=0 36 | for ip in ${ips}; do 37 | i=$(($i+1)) 38 | altnames+="IP.${i} = ${ip}"$'\n' 39 | subjectaltline="subjectAltName = @alt_names" 40 | done 41 | 42 | cat >"${config}" < 2 | 15 | 16 | 19 | 4.0.0 20 | 21 | com.codahale 22 | grpc-proxy 23 | 0.1.0-SNAPSHOT 24 | 25 | 26 | 1.8 27 | 1.8 28 | UTF-8 29 | 1.8.0 30 | 9.4.8.v20171121 31 | 1.7.25 32 | 4.1.16.Final 33 | 2.9.3 34 | 35 | 36 | 37 | 38 | com.google.code.findbugs 39 | jsr305 40 | 3.0.2 41 | 42 | 43 | com.google.auto.value 44 | auto-value 45 | 1.5.3 46 | provided 47 | 48 | 49 | io.grpc 50 | grpc-core 51 | ${grpc.version} 52 | 53 | 54 | io.grpc 55 | grpc-netty 56 | ${grpc.version} 57 | 58 | 59 | io.grpc 60 | grpc-protobuf 61 | ${grpc.version} 62 | 63 | 64 | io.grpc 65 | grpc-stub 66 | ${grpc.version} 67 | 68 | 69 | io.netty 70 | netty-handler 71 | ${netty.version} 72 | 73 | 74 | io.netty 75 | netty-transport 76 | ${netty.version} 77 | 78 | 79 | io.netty 80 | netty-tcnative 81 | 2.0.7.Final 82 | ${os.detected.classifier} 83 | 84 | 85 | io.netty 86 | netty-transport-native-epoll 87 | ${netty.version} 88 | 89 | linux-x86_64 90 | 91 | 92 | com.squareup.okhttp3 93 | okhttp 94 | 3.9.1 95 | 96 | 97 | org.eclipse.jetty 98 | jetty-server 99 | ${jetty.version} 100 | 101 | 102 | org.eclipse.jetty 103 | jetty-util 104 | ${jetty.version} 105 | 106 | 107 | org.hdrhistogram 108 | HdrHistogram 109 | 2.1.10 110 | 111 | 112 | ch.qos.logback 113 | logback-classic 114 | 1.2.3 115 | 116 | 117 | org.slf4j 118 | slf4j-api 119 | ${slf4j.version} 120 | 121 | 122 | org.slf4j 123 | jul-to-slf4j 124 | ${slf4j.version} 125 | 126 | 127 | net.logstash.logback 128 | logstash-logback-encoder 129 | 4.11 130 | 131 | 132 | io.airlift 133 | airline 134 | 0.8 135 | 136 | 137 | com.google.code.findbugs 138 | annotations 139 | 140 | 141 | 142 | 143 | com.fasterxml.jackson.core 144 | jackson-annotations 145 | ${jackson.version} 146 | 147 | 148 | com.fasterxml.jackson.core 149 | jackson-core 150 | ${jackson.version} 151 | 152 | 153 | com.fasterxml.jackson.core 154 | jackson-databind 155 | ${jackson.version} 156 | 157 | 158 | com.google.guava 159 | guava 160 | 23.6-jre 161 | 162 | 163 | javax.servlet 164 | javax.servlet-api 165 | 4.0.0 166 | 167 | 168 | com.google.protobuf 169 | protobuf-java 170 | 3.5.1 171 | 172 | 173 | 174 | 175 | 176 | kr.motd.maven 177 | os-maven-plugin 178 | 1.4.1.Final 179 | 180 | 181 | 182 | 183 | org.xolstice.maven.plugins 184 | protobuf-maven-plugin 185 | 0.5.0 186 | 187 | 188 | com.google.protobuf:protoc:3.3.0:exe:${os.detected.classifier} 189 | 190 | grpc-java 191 | 192 | io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} 193 | 194 | 195 | 196 | 197 | 198 | compile 199 | compile-custom 200 | 201 | 202 | 203 | 204 | 205 | org.apache.maven.plugins 206 | maven-shade-plugin 207 | 3.0.0 208 | 209 | false 210 | 211 | 213 | com.codahale.grpcproxy.Runner 214 | 215 | 216 | 217 | 218 | 219 | package 220 | 221 | shade 222 | 223 | 224 | 225 | 226 | 227 | org.apache.maven.plugins 228 | maven-compiler-plugin 229 | 3.7.0 230 | 231 | javac-with-errorprone 232 | true 233 | 8 234 | 8 235 | 236 | 237 | 238 | org.codehaus.plexus 239 | plexus-compiler-javac-errorprone 240 | 2.8.1 241 | 242 | 243 | com.google.errorprone 244 | error_prone_core 245 | 2.1.1 246 | 247 | 248 | 249 | 250 | org.apache.maven.plugins 251 | maven-dependency-plugin 252 | 3.0.0 253 | 254 | 255 | analyze 256 | 257 | analyze-only 258 | 259 | 260 | true 261 | 262 | com.google.auto.value:auto-value 263 | ch.qos.logback:logback-classic 264 | io.netty:netty-tcnative 265 | com.fasterxml.jackson.core:jackson-databind 266 | 267 | 268 | 269 | 270 | 271 | 272 | org.apache.maven.plugins 273 | maven-enforcer-plugin 274 | 1.4.1 275 | 276 | 277 | enforce 278 | 279 | 280 | 281 | 282 | 283 | 284 | enforce 285 | 286 | 287 | 288 | 289 | 290 | com.coveo 291 | fmt-maven-plugin 292 | 2.1.0 293 | 294 | 295 | 296 | format 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | -------------------------------------------------------------------------------- /src/main/java/com/codahale/grpcproxy/GreeterService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package com.codahale.grpcproxy; 16 | 17 | import com.codahale.grpcproxy.helloworld.GreeterGrpc; 18 | import com.codahale.grpcproxy.helloworld.HelloReply; 19 | import com.codahale.grpcproxy.helloworld.HelloRequest; 20 | import io.grpc.stub.StreamObserver; 21 | 22 | class GreeterService extends GreeterGrpc.GreeterImplBase { 23 | 24 | @Override 25 | public void sayHello(HelloRequest request, StreamObserver responseObserver) { 26 | final String message = "Hello " + request.getName(); 27 | responseObserver.onNext(HelloReply.newBuilder().setMessage(message).build()); 28 | responseObserver.onCompleted(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/codahale/grpcproxy/HelloWorldClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package com.codahale.grpcproxy; 16 | 17 | import com.codahale.grpcproxy.helloworld.GreeterGrpc; 18 | import com.codahale.grpcproxy.helloworld.HelloRequest; 19 | import com.codahale.grpcproxy.stats.Recorder; 20 | import com.codahale.grpcproxy.stats.Snapshot; 21 | import com.codahale.grpcproxy.util.Netty; 22 | import com.codahale.grpcproxy.util.TlsContext; 23 | import io.airlift.airline.Command; 24 | import io.airlift.airline.Option; 25 | import io.grpc.ManagedChannel; 26 | import io.grpc.StatusRuntimeException; 27 | import io.grpc.netty.NettyChannelBuilder; 28 | import io.netty.channel.EventLoopGroup; 29 | import java.time.Duration; 30 | import java.time.Instant; 31 | import java.util.concurrent.ExecutorService; 32 | import java.util.concurrent.Executors; 33 | import java.util.concurrent.TimeUnit; 34 | import javax.net.ssl.SSLException; 35 | import net.logstash.logback.marker.Markers; 36 | import org.slf4j.Logger; 37 | import org.slf4j.LoggerFactory; 38 | 39 | /** A gRPC client. This could be in any language. */ 40 | class HelloWorldClient { 41 | 42 | private static final Logger LOGGER = LoggerFactory.getLogger(HelloWorldClient.class); 43 | private final EventLoopGroup eventLoopGroup; 44 | private final ManagedChannel channel; 45 | private final GreeterGrpc.GreeterBlockingStub blockingStub; 46 | 47 | private HelloWorldClient(String host, int port, TlsContext tls) throws SSLException { 48 | this.eventLoopGroup = Netty.newWorkerEventLoopGroup(); 49 | this.channel = 50 | NettyChannelBuilder.forAddress(host, port) 51 | .eventLoopGroup(eventLoopGroup) 52 | .channelType(Netty.clientChannelType()) 53 | .sslContext(tls.toClientContext()) 54 | .build(); 55 | this.blockingStub = GreeterGrpc.newBlockingStub(channel); 56 | } 57 | 58 | private void shutdown() throws InterruptedException { 59 | channel.shutdown().awaitTermination(5, TimeUnit.SECONDS); 60 | eventLoopGroup.shutdownGracefully(0, 0, TimeUnit.SECONDS); 61 | } 62 | 63 | private String greet(int i) { 64 | try { 65 | final HelloRequest request = HelloRequest.newBuilder().setName("world " + i).build(); 66 | return blockingStub.sayHello(request).getMessage(); 67 | } catch (StatusRuntimeException e) { 68 | LOGGER.warn("RPC failed: {}", e.getStatus()); 69 | return null; 70 | } 71 | } 72 | 73 | @Command(name = "client", description = "Runs a bunch of HelloWorld client calls.") 74 | public static class Cmd implements Runnable { 75 | 76 | @Option( 77 | name = {"-h", "--hostname"}, 78 | description = "the hostname of the gRPC server" 79 | ) 80 | private String hostname = "localhost"; 81 | 82 | @Option( 83 | name = {"-p", "--port"}, 84 | description = "the port of the gRPC server" 85 | ) 86 | private int port = 50051; 87 | 88 | @Option( 89 | name = {"-n", "--requests"}, 90 | description = "the number of requests to make" 91 | ) 92 | private int requests = 1_000_000; 93 | 94 | @Option( 95 | name = {"-c", "--threads"}, 96 | description = "the number of threads to use" 97 | ) 98 | private int threads = 10; 99 | 100 | @Option(name = "--ca-certs") 101 | private String trustedCertsPath = "cert.crt"; 102 | 103 | @Option(name = "--cert") 104 | private String certPath = "cert.crt"; 105 | 106 | @Option(name = "--key") 107 | private String keyPath = "cert.key"; 108 | 109 | @Override 110 | public void run() { 111 | try { 112 | final TlsContext tls = new TlsContext(trustedCertsPath, certPath, keyPath); 113 | final HelloWorldClient client = new HelloWorldClient(hostname, port, tls); 114 | try { 115 | final Recorder recorder = 116 | new Recorder( 117 | 500, 118 | TimeUnit.MINUTES.toMicros(1), 119 | TimeUnit.MILLISECONDS.toMicros(10), 120 | TimeUnit.MICROSECONDS); 121 | LOGGER.info("Initial request: {}", client.greet(requests)); 122 | LOGGER.info("Sending {} requests from {} threads", requests, threads); 123 | 124 | final ExecutorService threadPool = Executors.newFixedThreadPool(threads); 125 | final Instant start = Instant.now(); 126 | for (int i = 0; i < threads; i++) { 127 | threadPool.execute( 128 | () -> { 129 | for (int j = 0; j < requests / threads; j++) { 130 | final long t = System.nanoTime(); 131 | client.greet(j); 132 | recorder.record(t); 133 | } 134 | }); 135 | } 136 | threadPool.shutdown(); 137 | threadPool.awaitTermination(20, TimeUnit.MINUTES); 138 | 139 | final Snapshot stats = recorder.interval(); 140 | final Duration duration = Duration.between(start, Instant.now()); 141 | LOGGER.info( 142 | Markers.append("stats", stats).and(Markers.append("duration", duration.toString())), 143 | "{} requests in {} ({} req/sec)", 144 | stats.count(), 145 | duration, 146 | stats.throughput()); 147 | } finally { 148 | client.shutdown(); 149 | } 150 | } catch (SSLException | InterruptedException e) { 151 | LOGGER.error("Error running command", e); 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/main/java/com/codahale/grpcproxy/HelloWorldServer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package com.codahale.grpcproxy; 16 | 17 | import com.codahale.grpcproxy.util.Netty; 18 | import com.codahale.grpcproxy.util.StatsTracerFactory; 19 | import com.codahale.grpcproxy.util.TlsContext; 20 | import io.airlift.airline.Command; 21 | import io.airlift.airline.Option; 22 | import io.grpc.Server; 23 | import io.grpc.netty.NettyServerBuilder; 24 | import io.netty.channel.EventLoopGroup; 25 | import java.io.IOException; 26 | import java.util.concurrent.TimeUnit; 27 | import javax.net.ssl.SSLException; 28 | import org.slf4j.Logger; 29 | import org.slf4j.LoggerFactory; 30 | 31 | class HelloWorldServer { 32 | 33 | private static final Logger LOGGER = LoggerFactory.getLogger(ProxyRpcServer.class); 34 | private final EventLoopGroup bossEventLoopGroup; 35 | private final EventLoopGroup workerEventLoopGroup; 36 | private final Server server; 37 | private final StatsTracerFactory stats; 38 | 39 | private HelloWorldServer(int port, TlsContext tls) throws SSLException { 40 | this.stats = new StatsTracerFactory(); 41 | this.bossEventLoopGroup = Netty.newBossEventLoopGroup(); 42 | this.workerEventLoopGroup = Netty.newWorkerEventLoopGroup(); 43 | this.server = 44 | NettyServerBuilder.forPort(port) 45 | .bossEventLoopGroup(bossEventLoopGroup) 46 | .workerEventLoopGroup(workerEventLoopGroup) 47 | .channelType(Netty.serverChannelType()) 48 | .addStreamTracerFactory(stats) 49 | .sslContext(tls.toServerContext()) 50 | .addService(new GreeterService()) 51 | .build(); 52 | } 53 | 54 | private void start() throws IOException, InterruptedException { 55 | stats.start(); 56 | server.start(); 57 | LOGGER.info("Server started, listening on {}", server.getPort()); 58 | Runtime.getRuntime().addShutdownHook(new Thread(this::stop)); 59 | server.awaitTermination(); 60 | } 61 | 62 | private void stop() { 63 | stats.stop(); 64 | if (!server.isShutdown()) { 65 | server.shutdown(); 66 | } 67 | bossEventLoopGroup.shutdownGracefully(0, 0, TimeUnit.SECONDS); 68 | workerEventLoopGroup.shutdownGracefully(0, 0, TimeUnit.SECONDS); 69 | } 70 | 71 | @Command(name = "grpc", description = "Run a gRPC HelloWorld service.") 72 | public static class Cmd implements Runnable { 73 | 74 | @Option( 75 | name = {"-p", "--port"}, 76 | description = "the port to listen on" 77 | ) 78 | private int port = 50051; 79 | 80 | @Option(name = "--ca-certs") 81 | private String trustedCertsPath = "cert.crt"; 82 | 83 | @Option(name = "--cert") 84 | private String certPath = "cert.crt"; 85 | 86 | @Option(name = "--key") 87 | private String keyPath = "cert.key"; 88 | 89 | @Override 90 | public void run() { 91 | try { 92 | final TlsContext tls = new TlsContext(trustedCertsPath, certPath, keyPath); 93 | final HelloWorldServer server = new HelloWorldServer(port, tls); 94 | server.start(); 95 | } catch (IOException | InterruptedException e) { 96 | LOGGER.error("Error running command", e); 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/codahale/grpcproxy/LegacyHttpServer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package com.codahale.grpcproxy; 16 | 17 | import com.codahale.grpcproxy.helloworld.HelloReply; 18 | import com.codahale.grpcproxy.helloworld.HelloRequest; 19 | import io.airlift.airline.Command; 20 | import io.airlift.airline.Option; 21 | import java.io.IOException; 22 | import javax.servlet.http.HttpServletRequest; 23 | import javax.servlet.http.HttpServletResponse; 24 | import org.eclipse.jetty.server.Request; 25 | import org.eclipse.jetty.server.Server; 26 | import org.eclipse.jetty.server.ServerConnector; 27 | import org.eclipse.jetty.server.handler.AbstractHandler; 28 | import org.eclipse.jetty.util.thread.QueuedThreadPool; 29 | import org.slf4j.Logger; 30 | import org.slf4j.LoggerFactory; 31 | 32 | /** 33 | * An HTTP/1.1 server which parses protobuf messages in request bodies and emits protobuf messages 34 | * in response bodies. Implements, in its own way, the {@code helloworld.Greeter} service. 35 | */ 36 | class LegacyHttpServer { 37 | 38 | private static final Logger LOGGER = LoggerFactory.getLogger(LegacyHttpServer.class); 39 | private final Server server; 40 | 41 | private LegacyHttpServer(int port, int threads) { 42 | this.server = new Server(new QueuedThreadPool(threads)); 43 | server.setHandler( 44 | new AbstractHandler() { 45 | @Override 46 | public void handle( 47 | String target, 48 | Request baseRequest, 49 | HttpServletRequest request, 50 | HttpServletResponse response) 51 | throws IOException { 52 | final String method = baseRequest.getParameter("method"); 53 | if ("helloworld.Greeter/SayHello".equals(method)) { 54 | baseRequest.setHandled(true); 55 | sayHello(baseRequest, response); 56 | } 57 | } 58 | }); 59 | 60 | final ServerConnector connector = new ServerConnector(server); 61 | connector.setPort(port); 62 | server.addConnector(connector); 63 | } 64 | 65 | private void start() throws Exception { 66 | Runtime.getRuntime().addShutdownHook(new Thread(this::stop)); 67 | server.start(); 68 | } 69 | 70 | private void stop() { 71 | try { 72 | server.stop(); 73 | } catch (Exception e) { 74 | LOGGER.error("Error shutting down server", e); 75 | } 76 | } 77 | 78 | private void sayHello(Request request, HttpServletResponse response) throws IOException { 79 | final HelloRequest req = HelloRequest.parseFrom(request.getInputStream()); 80 | final String greeting = "Hello " + req.getName(); 81 | final HelloReply resp = HelloReply.newBuilder().setMessage(greeting).build(); 82 | resp.writeTo(response.getOutputStream()); 83 | } 84 | 85 | @Command(name = "http", description = "Run a legacy HTTP/Protobuf HelloWorld service.") 86 | public static class Cmd implements Runnable { 87 | 88 | @Option( 89 | name = {"-p", "--port"}, 90 | description = "the port to listen on" 91 | ) 92 | private int port = 8080; 93 | 94 | @Option( 95 | name = {"-c", "--threads"}, 96 | description = "the number of worker threads to use" 97 | ) 98 | private int threads = 100; 99 | 100 | @Override 101 | public void run() { 102 | final LegacyHttpServer server = new LegacyHttpServer(port, threads); 103 | try { 104 | server.start(); 105 | } catch (Exception e) { 106 | LOGGER.error("Error starting server", e); 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/codahale/grpcproxy/ProxyHandlerRegistry.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package com.codahale.grpcproxy; 16 | 17 | import com.codahale.grpcproxy.util.ByteArrayMarshaller; 18 | import io.grpc.HandlerRegistry; 19 | import io.grpc.MethodDescriptor; 20 | import io.grpc.MethodDescriptor.MethodType; 21 | import io.grpc.ServerMethodDefinition; 22 | import io.grpc.stub.ServerCalls; 23 | import io.grpc.stub.StreamObserver; 24 | import java.io.IOException; 25 | import java.util.concurrent.TimeUnit; 26 | import javax.annotation.Nullable; 27 | import okhttp3.ConnectionPool; 28 | import okhttp3.HttpUrl; 29 | import okhttp3.MediaType; 30 | import okhttp3.OkHttpClient; 31 | import okhttp3.Request; 32 | import okhttp3.RequestBody; 33 | import okhttp3.Response; 34 | import okhttp3.ResponseBody; 35 | 36 | /** A handler registry which maps gRPC service methods to proxy listeners. */ 37 | class ProxyHandlerRegistry extends HandlerRegistry { 38 | 39 | private static final MediaType OCTET_STREAM = MediaType.parse("application/octet-stream"); 40 | private final HttpUrl backend; 41 | private final OkHttpClient client; 42 | 43 | ProxyHandlerRegistry(HttpUrl backend) { 44 | this.backend = backend; 45 | final ConnectionPool connectionPool = new ConnectionPool(100, 5, TimeUnit.MINUTES); 46 | this.client = new OkHttpClient.Builder().connectionPool(connectionPool).build(); 47 | } 48 | 49 | @Override 50 | public ServerMethodDefinition lookupMethod(String methodName, @Nullable String authority) { 51 | return ServerMethodDefinition.create( 52 | MethodDescriptor.newBuilder() 53 | .setRequestMarshaller(new ByteArrayMarshaller()) 54 | .setResponseMarshaller(new ByteArrayMarshaller()) 55 | .setType(MethodType.UNARY) 56 | .setFullMethodName(methodName) 57 | .build(), 58 | ServerCalls.asyncUnaryCall(new ProxyUnaryMethod(backend, methodName))); 59 | } 60 | 61 | /** Proxies a gRPC request to an HTTP backend. */ 62 | private class ProxyUnaryMethod implements ServerCalls.UnaryMethod { 63 | 64 | private final HttpUrl url; 65 | 66 | ProxyUnaryMethod(HttpUrl backend, String methodName) { 67 | this.url = backend.newBuilder().addQueryParameter("method", methodName).build(); 68 | } 69 | 70 | @Override 71 | public void invoke(byte[] msg, StreamObserver responseObserver) { 72 | final RequestBody reqBody = RequestBody.create(OCTET_STREAM, msg); 73 | final Request req = new Request.Builder().url(url).post(reqBody).build(); 74 | try { 75 | try (Response resp = client.newCall(req).execute()) { 76 | final ResponseBody respBody = resp.body(); 77 | if (respBody != null) { 78 | responseObserver.onNext(respBody.bytes()); 79 | } 80 | } 81 | responseObserver.onCompleted(); 82 | } catch (IOException e) { 83 | responseObserver.onError(e); 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/codahale/grpcproxy/ProxyRpcServer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package com.codahale.grpcproxy; 16 | 17 | import com.codahale.grpcproxy.util.Netty; 18 | import com.codahale.grpcproxy.util.StatsTracerFactory; 19 | import com.codahale.grpcproxy.util.TlsContext; 20 | import io.airlift.airline.Command; 21 | import io.airlift.airline.Option; 22 | import io.grpc.Server; 23 | import io.grpc.netty.NettyServerBuilder; 24 | import io.netty.channel.EventLoopGroup; 25 | import java.io.IOException; 26 | import java.util.concurrent.TimeUnit; 27 | import javax.net.ssl.SSLException; 28 | import okhttp3.HttpUrl; 29 | import org.slf4j.Logger; 30 | import org.slf4j.LoggerFactory; 31 | 32 | /** A gRPC server which proxies requests to an HTTP/1.1 backend server. */ 33 | class ProxyRpcServer { 34 | 35 | private static final Logger LOGGER = LoggerFactory.getLogger(ProxyRpcServer.class); 36 | private final EventLoopGroup bossEventLoopGroup; 37 | private final EventLoopGroup workerEventLoopGroup; 38 | private final Server server; 39 | private final StatsTracerFactory stats; 40 | 41 | private ProxyRpcServer(int port, TlsContext tls, HttpUrl backend) throws SSLException { 42 | this.stats = new StatsTracerFactory(); 43 | this.bossEventLoopGroup = Netty.newBossEventLoopGroup(); 44 | this.workerEventLoopGroup = Netty.newWorkerEventLoopGroup(); 45 | this.server = 46 | NettyServerBuilder.forPort(port) 47 | .bossEventLoopGroup(bossEventLoopGroup) 48 | .workerEventLoopGroup(workerEventLoopGroup) 49 | .channelType(Netty.serverChannelType()) 50 | .addStreamTracerFactory(stats) 51 | .sslContext(tls.toServerContext()) 52 | .fallbackHandlerRegistry(new ProxyHandlerRegistry(backend)) 53 | .build(); 54 | } 55 | 56 | private void start() throws IOException { 57 | stats.start(); 58 | server.start(); 59 | LOGGER.info("Server started, listening on " + server.getPort()); 60 | Runtime.getRuntime().addShutdownHook(new Thread(this::stop)); 61 | } 62 | 63 | private void stop() { 64 | stats.stop(); 65 | if (!server.isShutdown()) { 66 | server.shutdown(); 67 | } 68 | bossEventLoopGroup.shutdownGracefully(0, 0, TimeUnit.SECONDS); 69 | workerEventLoopGroup.shutdownGracefully(0, 0, TimeUnit.SECONDS); 70 | } 71 | 72 | private void blockUntilShutdown() throws InterruptedException { 73 | server.awaitTermination(); 74 | } 75 | 76 | @Command(name = "proxy", description = "Run a gRPC proxy server.") 77 | public static class Cmd implements Runnable { 78 | 79 | @Option( 80 | name = {"-p", "--port"}, 81 | description = "the port to listen on" 82 | ) 83 | private int port = 50051; 84 | 85 | @Option( 86 | name = {"-u", "--upstream"}, 87 | description = "the URL of the upstream HTTP server" 88 | ) 89 | private String upstream = "http://localhost:8080/grpc"; 90 | 91 | @Option(name = "--ca-certs") 92 | private String trustedCertsPath = "cert.crt"; 93 | 94 | @Option(name = "--cert") 95 | private String certPath = "cert.crt"; 96 | 97 | @Option(name = "--key") 98 | private String keyPath = "cert.key"; 99 | 100 | @Override 101 | public void run() { 102 | try { 103 | final TlsContext tls = new TlsContext(trustedCertsPath, certPath, keyPath); 104 | final ProxyRpcServer server = new ProxyRpcServer(port, tls, HttpUrl.parse(upstream)); 105 | server.start(); 106 | server.blockUntilShutdown(); 107 | } catch (IOException | InterruptedException e) { 108 | LOGGER.error("Error running command", e); 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/codahale/grpcproxy/Runner.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package com.codahale.grpcproxy; 16 | 17 | import io.airlift.airline.Cli; 18 | import io.airlift.airline.Cli.CliBuilder; 19 | import io.airlift.airline.Help; 20 | import org.slf4j.bridge.SLF4JBridgeHandler; 21 | 22 | public class Runner { 23 | 24 | public static void main(String[] args) { 25 | SLF4JBridgeHandler.removeHandlersForRootLogger(); 26 | SLF4JBridgeHandler.install(); 27 | cli().parse(args).run(); 28 | } 29 | 30 | private static Cli cli() { 31 | final CliBuilder builder = Cli.builder("grpc-proxy"); 32 | 33 | builder 34 | .withDescription("A set of example services for testing a gRPC proxy service.") 35 | .withDefaultCommand(Help.class) 36 | .withCommand(Help.class) 37 | .withCommand(HelloWorldClient.Cmd.class); 38 | 39 | builder 40 | .withGroup("server") 41 | .withDescription("Run a server") 42 | .withDefaultCommand(Help.class) 43 | .withCommand(Help.class) 44 | .withCommand(ProxyRpcServer.Cmd.class) 45 | .withCommand(LegacyHttpServer.Cmd.class) 46 | .withCommand(HelloWorldServer.Cmd.class); 47 | 48 | return builder.build(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/codahale/grpcproxy/stats/IntervalAdder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package com.codahale.grpcproxy.stats; 16 | 17 | import java.util.concurrent.atomic.LongAdder; 18 | 19 | public class IntervalAdder { 20 | 21 | private final LongAdder count; 22 | private volatile long timestamp; 23 | 24 | public IntervalAdder() { 25 | this.count = new LongAdder(); 26 | this.timestamp = System.nanoTime(); 27 | } 28 | 29 | public void add(long x) { 30 | count.add(x); 31 | } 32 | 33 | public IntervalCount interval() { 34 | final long n = count.sumThenReset(); 35 | final long t = System.nanoTime(); 36 | final double i = (t - timestamp) * 1e-9; 37 | this.timestamp = t; 38 | 39 | return new AutoValue_IntervalCount(n / i, n); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/codahale/grpcproxy/stats/IntervalCount.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package com.codahale.grpcproxy.stats; 16 | 17 | import com.fasterxml.jackson.annotation.JsonProperty; 18 | import com.google.auto.value.AutoValue; 19 | 20 | @AutoValue 21 | public abstract class IntervalCount { 22 | 23 | @JsonProperty 24 | public abstract double rate(); 25 | 26 | @JsonProperty 27 | public abstract long count(); 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/codahale/grpcproxy/stats/Recorder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package com.codahale.grpcproxy.stats; 16 | 17 | import java.util.concurrent.TimeUnit; 18 | import org.HdrHistogram.Histogram; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | 22 | public class Recorder { 23 | 24 | private static final Logger LOGGER = LoggerFactory.getLogger(Recorder.class.getCanonicalName()); 25 | 26 | private final IntervalAdder count; 27 | private final IntervalAdder responseTime; 28 | private final org.HdrHistogram.Recorder latency; 29 | private final long goalLatency; 30 | private volatile Histogram histogram; 31 | 32 | public Recorder(long minLatency, long maxLatency, long goalLatency, TimeUnit latencyUnit) { 33 | this.goalLatency = latencyUnit.toMicros(goalLatency); 34 | this.count = new IntervalAdder(); 35 | this.responseTime = new IntervalAdder(); 36 | this.latency = 37 | new org.HdrHistogram.Recorder( 38 | latencyUnit.toMicros(minLatency), latencyUnit.toMicros(maxLatency), 1); 39 | this.histogram = latency.getIntervalHistogram(); // preload reporting histogram 40 | } 41 | 42 | public void record(long startNanoTime) { 43 | final long duration = TimeUnit.NANOSECONDS.toMicros(System.nanoTime() - startNanoTime); 44 | count.add(1); 45 | responseTime.add(duration); 46 | try { 47 | latency.recordValue(duration); 48 | } catch (ArrayIndexOutOfBoundsException ignored) { 49 | LOGGER.warn("Very slow value: {}us", duration); 50 | } 51 | } 52 | 53 | public Snapshot interval() { 54 | final IntervalCount requestCount = count.interval(); 55 | final IntervalCount responseTimeCount = responseTime.interval(); 56 | final Histogram h = latency.getIntervalHistogram(histogram); 57 | final long c = requestCount.count(); 58 | final double x = requestCount.rate(); 59 | final long satisfied = h.getCountBetweenValues(0, goalLatency); 60 | final long tolerating = h.getCountBetweenValues(goalLatency, goalLatency * 4); 61 | final double p50 = h.getValueAtPercentile(50) * 1e-6; 62 | final double p90 = h.getValueAtPercentile(90) * 1e-6; 63 | final double p99 = h.getValueAtPercentile(99) * 1e-6; 64 | final double p999 = h.getValueAtPercentile(99.9) * 1e-6; 65 | this.histogram = h; 66 | final double r, n, apdex; 67 | if (c == 0) { 68 | r = n = apdex = 0; 69 | } else { 70 | r = responseTimeCount.rate() / c * 1e-6; 71 | n = x * r; 72 | apdex = Math.min(1.0, (satisfied + (tolerating / 2.0)) / c); 73 | } 74 | return new AutoValue_Snapshot(c, x, n, r, p50, p90, p99, p999, apdex); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/codahale/grpcproxy/stats/Snapshot.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package com.codahale.grpcproxy.stats; 16 | 17 | import com.fasterxml.jackson.annotation.JsonProperty; 18 | import com.google.auto.value.AutoValue; 19 | 20 | @AutoValue 21 | public abstract class Snapshot { 22 | 23 | @JsonProperty 24 | public abstract long count(); 25 | 26 | @JsonProperty 27 | public abstract double throughput(); 28 | 29 | @JsonProperty 30 | public abstract double concurrency(); 31 | 32 | @JsonProperty 33 | public abstract double latency(); 34 | 35 | @JsonProperty 36 | public abstract double p50(); 37 | 38 | @JsonProperty 39 | public abstract double p90(); 40 | 41 | @JsonProperty 42 | public abstract double p99(); 43 | 44 | @JsonProperty 45 | public abstract double p999(); 46 | 47 | @JsonProperty 48 | public abstract double apdex(); 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/codahale/grpcproxy/util/ByteArrayMarshaller.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package com.codahale.grpcproxy.util; 16 | 17 | import com.google.common.io.ByteStreams; 18 | import io.grpc.MethodDescriptor.Marshaller; 19 | import java.io.ByteArrayInputStream; 20 | import java.io.IOException; 21 | import java.io.InputStream; 22 | 23 | /** A dumb marshaller which refuses to marshal. */ 24 | public class ByteArrayMarshaller implements Marshaller { 25 | 26 | @Override 27 | public InputStream stream(byte[] value) { 28 | return new ByteArrayInputStream(value); 29 | } 30 | 31 | @Override 32 | public byte[] parse(InputStream stream) { 33 | try { 34 | return ByteStreams.toByteArray(stream); 35 | } catch (IOException e) { 36 | throw new RuntimeException(e); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/codahale/grpcproxy/util/Netty.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package com.codahale.grpcproxy.util; 16 | 17 | import io.netty.channel.Channel; 18 | import io.netty.channel.EventLoopGroup; 19 | import io.netty.channel.ServerChannel; 20 | import io.netty.channel.epoll.Epoll; 21 | import io.netty.channel.epoll.EpollEventLoopGroup; 22 | import io.netty.channel.epoll.EpollServerSocketChannel; 23 | import io.netty.channel.epoll.EpollSocketChannel; 24 | import io.netty.channel.nio.NioEventLoopGroup; 25 | import io.netty.channel.socket.nio.NioServerSocketChannel; 26 | import io.netty.channel.socket.nio.NioSocketChannel; 27 | import org.slf4j.Logger; 28 | import org.slf4j.LoggerFactory; 29 | 30 | public class Netty { 31 | 32 | private static final Logger LOGGER = LoggerFactory.getLogger(Netty.class); 33 | private static final int WORKER_THREADS = Runtime.getRuntime().availableProcessors() * 2; 34 | 35 | static { 36 | if (Epoll.isAvailable()) { 37 | LOGGER.info("Using epoll"); 38 | } else { 39 | LOGGER.info("Using java.nio"); 40 | } 41 | } 42 | 43 | public static EventLoopGroup newBossEventLoopGroup() { 44 | if (Epoll.isAvailable()) { 45 | return new EpollEventLoopGroup(); 46 | } 47 | return new NioEventLoopGroup(); 48 | } 49 | 50 | public static EventLoopGroup newWorkerEventLoopGroup() { 51 | if (Epoll.isAvailable()) { 52 | return new EpollEventLoopGroup(WORKER_THREADS); 53 | } 54 | return new NioEventLoopGroup(WORKER_THREADS); 55 | } 56 | 57 | public static Class serverChannelType() { 58 | if (Epoll.isAvailable()) { 59 | return EpollServerSocketChannel.class; 60 | } 61 | return NioServerSocketChannel.class; 62 | } 63 | 64 | public static Class clientChannelType() { 65 | if (Epoll.isAvailable()) { 66 | return EpollSocketChannel.class; 67 | } 68 | return NioSocketChannel.class; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/codahale/grpcproxy/util/PrettyPrintingDecorator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package com.codahale.grpcproxy.util; 16 | 17 | import com.fasterxml.jackson.core.JsonGenerator; 18 | import net.logstash.logback.decorate.JsonGeneratorDecorator; 19 | 20 | public class PrettyPrintingDecorator implements JsonGeneratorDecorator { 21 | 22 | @Override 23 | public JsonGenerator decorate(JsonGenerator generator) { 24 | if (Boolean.valueOf(System.getenv("DENSE_LOGS"))) { 25 | return generator; 26 | } 27 | return generator.useDefaultPrettyPrinter(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/codahale/grpcproxy/util/StatsTracerFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package com.codahale.grpcproxy.util; 16 | 17 | import com.codahale.grpcproxy.stats.IntervalAdder; 18 | import com.codahale.grpcproxy.stats.Recorder; 19 | import io.grpc.Metadata; 20 | import io.grpc.ServerStreamTracer; 21 | import io.grpc.Status; 22 | import java.util.Map.Entry; 23 | import java.util.concurrent.ConcurrentHashMap; 24 | import java.util.concurrent.ConcurrentMap; 25 | import java.util.concurrent.Executors; 26 | import java.util.concurrent.ScheduledExecutorService; 27 | import java.util.concurrent.TimeUnit; 28 | import net.logstash.logback.marker.LogstashMarker; 29 | import net.logstash.logback.marker.Markers; 30 | import org.slf4j.Logger; 31 | import org.slf4j.LoggerFactory; 32 | 33 | /** 34 | * A stream tracer factory which measures throughput, concurrency, response time, and latency 35 | * distribution. 36 | */ 37 | public class StatsTracerFactory extends ServerStreamTracer.Factory { 38 | 39 | private static final Logger LOGGER = LoggerFactory.getLogger(StatsTracerFactory.class); 40 | private static final long MIN_DURATION = TimeUnit.MICROSECONDS.toMicros(500); 41 | private static final long GOAL_DURATION = TimeUnit.MILLISECONDS.toMicros(10); 42 | private static final long MAX_DURATION = TimeUnit.SECONDS.toMicros(30); 43 | 44 | private final IntervalAdder bytesIn = new IntervalAdder(); 45 | private final IntervalAdder bytesOut = new IntervalAdder(); 46 | private final Recorder all = newRecorder(); 47 | private final ConcurrentMap endpoints = new ConcurrentHashMap<>(); 48 | private ScheduledExecutorService executor; 49 | 50 | @Override 51 | public ServerStreamTracer newServerStreamTracer(String fullMethodName, Metadata headers) { 52 | 53 | return new ServerStreamTracer() { 54 | final long start = System.nanoTime(); 55 | final Recorder endpoint = endpoints.computeIfAbsent(fullMethodName, k -> newRecorder()); 56 | 57 | @Override 58 | public void outboundWireSize(long bytes) { 59 | bytesOut.add(bytes); 60 | } 61 | 62 | @Override 63 | public void inboundWireSize(long bytes) { 64 | bytesIn.add(bytes); 65 | } 66 | 67 | @Override 68 | public void streamClosed(Status status) { 69 | final double duration = (System.nanoTime() - start) * 1e-9; 70 | LOGGER.debug( 71 | Markers.append("grpc_method_name", fullMethodName) 72 | .and(Markers.append("status", status)) 73 | .and(Markers.append("duration", duration)), 74 | "request handled"); 75 | all.record(start); 76 | endpoint.record(start); 77 | } 78 | }; 79 | } 80 | 81 | public void start() { 82 | executor = Executors.newSingleThreadScheduledExecutor(); 83 | executor.scheduleAtFixedRate(this::report, 1, 1, TimeUnit.SECONDS); 84 | } 85 | 86 | public void stop() { 87 | executor.shutdown(); 88 | } 89 | 90 | /** 91 | * Calculate and report the three parameters of Little's Law and some latency percentiles. 92 | * 93 | *

This just writes them to stdout, but presumably we'd be reporting them to a centralized 94 | * service. 95 | */ 96 | private void report() { 97 | LogstashMarker marker = 98 | Markers.append("all", all.interval()) 99 | .and(Markers.append("bytes_in", bytesIn.interval())) 100 | .and(Markers.append("bytes_out", bytesOut.interval())); 101 | for (Entry entry : endpoints.entrySet()) { 102 | marker = marker.and(Markers.append(entry.getKey(), entry.getValue().interval())); 103 | } 104 | LOGGER.info(marker, "stats"); 105 | } 106 | 107 | private Recorder newRecorder() { 108 | return new Recorder(MIN_DURATION, MAX_DURATION, GOAL_DURATION, TimeUnit.MICROSECONDS); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/codahale/grpcproxy/util/TlsContext.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package com.codahale.grpcproxy.util; 16 | 17 | import io.grpc.netty.GrpcSslContexts; 18 | import io.netty.handler.ssl.ClientAuth; 19 | import io.netty.handler.ssl.SslContext; 20 | import io.netty.handler.ssl.SslContextBuilder; 21 | import io.netty.handler.ssl.SslProvider; 22 | import java.io.File; 23 | import javax.net.ssl.SSLException; 24 | 25 | public class TlsContext { 26 | 27 | private final File trustedCerts, cert, key; 28 | 29 | public TlsContext(String trustedCertsPath, String certPath, String keyPath) { 30 | this.trustedCerts = new File(trustedCertsPath); 31 | if (!trustedCerts.exists()) { 32 | throw new IllegalArgumentException("Can't find " + trustedCertsPath); 33 | } 34 | 35 | this.cert = new File(certPath); 36 | if (!cert.exists()) { 37 | throw new IllegalArgumentException("Can't find " + certPath); 38 | } 39 | 40 | this.key = new File(keyPath); 41 | if (!key.exists()) { 42 | throw new IllegalArgumentException("Can't find " + keyPath); 43 | } 44 | } 45 | 46 | public SslContext toClientContext() throws SSLException { 47 | return GrpcSslContexts.configure(SslContextBuilder.forClient(), SslProvider.OPENSSL) 48 | .trustManager(trustedCerts) 49 | .keyManager(cert, key) 50 | .build(); 51 | } 52 | 53 | public SslContext toServerContext() throws SSLException { 54 | return GrpcSslContexts.configure(SslContextBuilder.forServer(cert, key), SslProvider.OPENSSL) 55 | .trustManager(trustedCerts) 56 | .clientAuth(ClientAuth.REQUIRE) 57 | .build(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/proto/helloworld.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option java_multiple_files = true; 4 | option java_package = "com.codahale.grpcproxy.helloworld"; 5 | option java_outer_classname = "HelloWorldProto"; 6 | 7 | package helloworld; 8 | 9 | // The greeting service definition. 10 | service Greeter { 11 | // Sends a greeting 12 | rpc SayHello (HelloRequest) returns (HelloReply) { 13 | } 14 | } 15 | 16 | // The request message containing the user's name. 17 | message HelloRequest { 18 | string name = 1; 19 | } 20 | 21 | // The response message containing the greetings 22 | message HelloReply { 23 | string message = 1; 24 | } -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | --------------------------------------------------------------------------------