├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── gruffalo-standalone.sh ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── outbrain │ │ └── gruffalo │ │ ├── StandaloneGruffaloServer.java │ │ ├── netty │ │ ├── Batch.java │ │ ├── DatagramPacketToStringDecoder.java │ │ ├── GraphiteChannelInboundHandler.java │ │ ├── GraphiteClient.java │ │ ├── GraphiteClientChannelInitializer.java │ │ ├── GraphiteClientPool.java │ │ ├── GruffaloProxy.java │ │ ├── LineBasedFrameDecoderFactory.java │ │ ├── MetricBatcher.java │ │ ├── MetricBatcherFactory.java │ │ ├── MetricPublishHandler.java │ │ ├── NettyGraphiteClient.java │ │ ├── NettyQueuesMetricsInitializer.java │ │ ├── TcpServerPipelineFactory.java │ │ ├── Throttler.java │ │ └── UdpServerPipelineFactory.java │ │ ├── publish │ │ ├── CompoundMetricsPublisher.java │ │ ├── GraphiteMetricsPublisher.java │ │ ├── MetricsPublisher.java │ │ └── TimedMetricsPublisher.java │ │ └── util │ │ └── HostName2MetricName.java └── resources │ ├── applicationContext-GruffaloLib-all.xml │ ├── applicationContext-GruffaloLib-configuration.xml │ ├── applicationContext-GruffaloLib-domain.xml │ ├── applicationContext-GruffaloLib-netty.xml │ ├── applicationContext-GruffaloLib-standalone-metrics.xml │ ├── stand-alone-demo.properties │ └── stand-alone-log4j.xml └── test ├── java └── com │ └── outbrain │ └── gruffalo │ ├── Benchmark.java │ ├── GruffaloTestIT.java │ ├── UDPBenchmark.java │ └── publish │ └── GraphiteMetricsPublisherTest.java └── resources └── log4j.xml /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | release.properties 3 | *.iml 4 | *.log 5 | .idea/ 6 | classpath 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk8 4 | - openjdk11 5 | cache: 6 | directories: 7 | - $HOME/.m2 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Gruffalo 2 | ======== 3 | [![Build Status](https://travis-ci.org/outbrain/gruffalo.svg?branch=master)](https://travis-ci.org/outbrain/gruffalo) 4 | 5 | Gruffalo is an asynchronous Netty based graphite proxy. 6 | It protects Graphite from the herds of clients by minimizing context switches and interrupts; by batching and aggregating metrics. 7 | 8 | Gruffalo also allows you to replicate metrics between Graphite installations for DR scenarios, for example. 9 | 10 | Gruffalo can easily handle a massive amount of traffic, and thus increase your metrics delivery system availability. 11 | 12 | At Outbrain, we currently handle over 1700 concurrent connections, and over 2M metrics per minute per instance. 13 | 14 | Reference Docs 15 | -------------- 16 | * [About the design] (http://www.slideshare.net/eranharel/reactive-by-example-at-reversim-summit-2015) 17 | -------------------------------------------------------------------------------- /gruffalo-standalone.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mvn dependency:build-classpath -Dmdep.outputFile=classpath 4 | export CLASSPATH=target/classes:`cat classpath` 5 | export log4jfile=$(pwd)/src/main/resources/stand-alone-log4j.xml 6 | echo $log4jfile 7 | java -cp $CLASSPATH -Dlog4j.configuration=file://$log4jfile -Xmx2024m -Xms2024m com.outbrain.gruffalo.StandaloneGruffaloServer 8 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | Gruffalo 5 | com.outbrain.swinfra 6 | jar 7 | Gruffalo 8 | 0.23-SNAPSHOT 9 | 10 | 11 | 12 | Apache License Version 2.0, January 2004 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | repo 15 | 16 | 17 | 18 | 19 | scm:git:https://github.com/outbrain/gruffalo.git 20 | scm:git:https://github.com/outbrain/gruffalo.git 21 | https://github.com/outbrain/gruffalo/ 22 | HEAD 23 | 24 | 25 | 26 | outbrain 27 | http://www.outbrain.com 28 | 29 | 30 | 31 | 32 | Eran Harel 33 | http://il.linkedin.com/pub/eran-harel/4/4a3/234/ 34 | eran@outbrain.com 35 | 36 | 37 | 38 | 39 | UTF-8 40 | 1.7 41 | 1.7 42 | 43 | 4.0.7.RELEASE 44 | 45 | 46 | 47 | 54 | 61 | 62 | 63 | 64 | 71 | 72 | jcenter 73 | jcenter 74 | https://jcenter.bintray.com 75 | false 76 | 77 | 78 | 79 | 80 | 81 | 82 | 97 | 98 | 99 | org.apache.maven.plugins 100 | maven-release-plugin 101 | 2.5.1 102 | 103 | 104 | 105 | org.apache.maven.plugins 106 | maven-compiler-plugin 107 | 3.1 108 | 109 | ${java.compiler.source} 110 | ${java.compiler.target} 111 | 112 | 113 | 114 | 115 | org.apache.maven.plugins 116 | maven-surefire-plugin 117 | 2.17 118 | 119 | false 120 | false 121 | once 122 | -Xms512m -Xmx2048m -XX:MaxPermSize=1024m 123 | 124 | 125 | 126 | 127 | 128 | org.apache.maven.plugins 129 | maven-jar-plugin 130 | 2.4 131 | 132 | 133 | 134 | ${maven.build.timestamp} 135 | 136 | 137 | 138 | 139 | 140 | 141 | make-a-jar 142 | compile 143 | 144 | jar 145 | 146 | 147 | 148 | make-a-test-jar 149 | 150 | test-jar 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | org.apache.maven.plugins 159 | maven-source-plugin 160 | 2.3 161 | 162 | 163 | attach-sources 164 | 165 | jar 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | com.outbrain.swinfra 178 | util-metrics 179 | 0.122 180 | 181 | 182 | 183 | com.outbrain.swinfra 184 | util-config 185 | 0.122 186 | 187 | 188 | 189 | io.netty 190 | netty-all 191 | 4.0.23.Final 192 | 193 | 194 | joda-time 195 | joda-time 196 | 2.2 197 | 198 | 199 | 200 | org.springframework 201 | spring-core 202 | ${org.springframework.version} 203 | 204 | 205 | org.springframework 206 | spring-context 207 | ${org.springframework.version} 208 | 209 | 210 | com.google.guava 211 | guava 212 | 16.0.1 213 | 214 | 215 | org.apache.commons 216 | commons-lang3 217 | 3.3.2 218 | 219 | 220 | com.codahale.metrics 221 | metrics-core 222 | 3.0.2 223 | 224 | 225 | 226 | org.slf4j 227 | slf4j-log4j12 228 | 1.7.5 229 | provided 230 | 231 | 232 | log4j 233 | log4j 234 | 1.2.17 235 | provided 236 | 237 | 238 | 239 | org.mockito 240 | mockito-all 241 | test 242 | 1.9.5 243 | 244 | 245 | 246 | -------------------------------------------------------------------------------- /src/main/java/com/outbrain/gruffalo/StandaloneGruffaloServer.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.context.support.ClassPathXmlApplicationContext; 6 | 7 | /** 8 | * Gruffalo Server main class 9 | * 10 | * @author Eran Harel 11 | */ 12 | public class StandaloneGruffaloServer { 13 | private static final Logger logger = LoggerFactory.getLogger(StandaloneGruffaloServer.class); 14 | 15 | public static void main(final String[] args) { 16 | new StandaloneGruffaloServer().start(); 17 | logger.info("******** Gruffalo started ********"); 18 | } 19 | 20 | public void start() { 21 | new ClassPathXmlApplicationContext("applicationContext-GruffaloLib-all.xml", "applicationContext-GruffaloLib-standalone-metrics.xml"); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/outbrain/gruffalo/netty/Batch.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo.netty; 2 | 3 | /** 4 | * Time: 8/5/13 11:23 AM 5 | * 6 | * @author Eran Harel 7 | */ 8 | class Batch { 9 | final StringBuilder payload; 10 | final int batchSize; 11 | 12 | Batch(StringBuilder payload, int batchSize) { 13 | this.payload = payload; 14 | this.batchSize = batchSize; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/outbrain/gruffalo/netty/DatagramPacketToStringDecoder.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo.netty; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import io.netty.channel.socket.DatagramPacket; 5 | import io.netty.handler.codec.MessageToMessageDecoder; 6 | import io.netty.util.CharsetUtil; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * Decodes a UDP packet to it's {@link String} payload. 12 | * @author eran 13 | */ 14 | public class DatagramPacketToStringDecoder extends MessageToMessageDecoder { 15 | 16 | @Override 17 | protected void decode(ChannelHandlerContext channelHandlerContext, DatagramPacket msg, List out) throws Exception { 18 | out.add(msg.content().toString(CharsetUtil.UTF_8)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/outbrain/gruffalo/netty/GraphiteChannelInboundHandler.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo.netty; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import com.google.common.base.Preconditions; 9 | 10 | import io.netty.channel.ChannelFuture; 11 | import io.netty.channel.ChannelFutureListener; 12 | import io.netty.channel.ChannelHandler; 13 | import io.netty.channel.ChannelHandlerContext; 14 | import io.netty.channel.EventLoop; 15 | import io.netty.channel.SimpleChannelInboundHandler; 16 | import io.netty.handler.timeout.IdleState; 17 | import io.netty.handler.timeout.IdleStateEvent; 18 | 19 | @ChannelHandler.Sharable 20 | public class GraphiteChannelInboundHandler extends SimpleChannelInboundHandler { 21 | 22 | private static final Logger log = LoggerFactory.getLogger(GraphiteChannelInboundHandler.class); 23 | private static final int RECONNECT_DELAY_SEC = 5; 24 | 25 | private final GraphiteClient client; 26 | 27 | private final String graphiteTarget; 28 | private final Throttler throttler; 29 | private boolean serverReadEnabled = true; 30 | 31 | private final ChannelFutureListener restoreServerReads = new ChannelFutureListener() { 32 | @Override 33 | public void operationComplete(final ChannelFuture future) throws Exception { 34 | throttler.restoreClientReads(); 35 | } 36 | }; 37 | 38 | public GraphiteChannelInboundHandler(final GraphiteClient client, final String graphiteTarget, final Throttler throttler) { 39 | this.client = Preconditions.checkNotNull(client, "client may not be null"); 40 | this.graphiteTarget = graphiteTarget; 41 | this.throttler = Preconditions.checkNotNull(throttler, "throttler must not be null"); 42 | } 43 | 44 | @Override 45 | protected void channelRead0(final ChannelHandlerContext ctx, final String msg) throws Exception { 46 | log.warn("Got an unexpected downstream message: " + msg); 47 | } 48 | 49 | @Override 50 | public void channelActive(final ChannelHandlerContext ctx) throws Exception { 51 | log.info("Connected to: {}", ctx.channel().remoteAddress()); 52 | } 53 | 54 | @Override 55 | public void channelUnregistered(final ChannelHandlerContext ctx) throws Exception { 56 | log.warn("Got disconnected from {}... will try to reconnect in {} sec...", graphiteTarget, RECONNECT_DELAY_SEC); 57 | scheduleReconnect(ctx); 58 | } 59 | 60 | private void scheduleReconnect(final ChannelHandlerContext ctx) { 61 | final EventLoop loop = ctx.channel().eventLoop(); 62 | loop.schedule(new Runnable() { 63 | @Override 64 | public void run() { 65 | log.info("Reconnecting to {}", graphiteTarget); 66 | client.connect(); 67 | } 68 | }, RECONNECT_DELAY_SEC, TimeUnit.SECONDS); 69 | } 70 | 71 | @Override 72 | public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) throws Exception { 73 | if (evt instanceof IdleStateEvent) { 74 | final IdleStateEvent e = (IdleStateEvent) evt; 75 | if (!serverReadEnabled && e.state() == IdleState.WRITER_IDLE) { 76 | // close the channel so we try to reconnect, and restore reads 77 | log.info("Outbound connection to {} seem disconnected. Closing channel and restoring server reads.", graphiteTarget); 78 | ctx.channel().close().addListener(restoreServerReads); 79 | } 80 | } 81 | } 82 | 83 | @Override 84 | public void channelWritabilityChanged(final ChannelHandlerContext ctx) throws Exception { 85 | final boolean autoread = ctx.channel().isWritable(); 86 | serverReadEnabled = autoread; 87 | if (!autoread) { 88 | client.onPushBack(); 89 | } 90 | 91 | throttler.changeServerAutoRead(autoread); 92 | } 93 | 94 | @Override 95 | public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) throws Exception { 96 | log.error("Unexpected exception from downstream.", cause); 97 | ctx.close(); 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/outbrain/gruffalo/netty/GraphiteClient.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo.netty; 2 | 3 | /** 4 | * @author Eran Harel 5 | */ 6 | public interface GraphiteClient { 7 | 8 | /** 9 | * Connects to the graphite relay 10 | */ 11 | public void connect(); 12 | 13 | public boolean publishMetrics(String metrics); 14 | 15 | /** 16 | * Notifies the client that the incoming requests are suspended due to slow writes 17 | */ 18 | public void onPushBack(); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/outbrain/gruffalo/netty/GraphiteClientChannelInitializer.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo.netty; 2 | 3 | import com.google.common.base.Preconditions; 4 | 5 | import io.netty.bootstrap.Bootstrap; 6 | import io.netty.buffer.UnpooledByteBufAllocator; 7 | import io.netty.channel.Channel; 8 | import io.netty.channel.ChannelFuture; 9 | import io.netty.channel.ChannelHandler; 10 | import io.netty.channel.ChannelInitializer; 11 | import io.netty.channel.ChannelOption; 12 | import io.netty.channel.ChannelPipeline; 13 | import io.netty.channel.EventLoopGroup; 14 | import io.netty.channel.socket.nio.NioSocketChannel; 15 | import io.netty.handler.codec.string.StringDecoder; 16 | import io.netty.handler.codec.string.StringEncoder; 17 | import io.netty.handler.timeout.IdleStateHandler; 18 | 19 | /** 20 | * Time: 8/4/13 10:06 AM 21 | * 22 | * @author Eran Harel 23 | */ 24 | public class GraphiteClientChannelInitializer extends ChannelInitializer { 25 | 26 | private final String graphiteHost; 27 | private final int graphitePort; 28 | private final EventLoopGroup eventLoopGroup; 29 | private final StringDecoder decoder; 30 | private final StringEncoder encoder; 31 | private ChannelHandler graphiteHandler; 32 | 33 | public GraphiteClientChannelInitializer(final String graphiteHost, final int graphitePort, final EventLoopGroup eventLoopGroup, 34 | final StringDecoder decoder, final StringEncoder encoder, final ChannelHandler graphiteHandler) { 35 | this.graphiteHost = graphiteHost; 36 | this.graphitePort = graphitePort; 37 | this.decoder = Preconditions.checkNotNull(decoder, "decoder must not be null"); 38 | this.encoder = Preconditions.checkNotNull(encoder, "encoder must not be null"); 39 | this.graphiteHandler = Preconditions.checkNotNull(graphiteHandler, "graphiteHandler must not be null"); 40 | this.eventLoopGroup = Preconditions.checkNotNull(eventLoopGroup, "eventLoopGroup must not be null"); 41 | } 42 | 43 | public ChannelFuture connect() { 44 | return configureBootstrap().connect(); 45 | } 46 | 47 | private Bootstrap configureBootstrap() { 48 | Bootstrap bootstrap = new Bootstrap(); 49 | bootstrap.remoteAddress(graphiteHost, graphitePort); 50 | bootstrap.group(eventLoopGroup); 51 | bootstrap.channel(NioSocketChannel.class); 52 | bootstrap.handler(this); 53 | bootstrap.option(ChannelOption.SO_LINGER, 0); 54 | bootstrap.option(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT); 55 | 56 | return bootstrap; 57 | } 58 | 59 | @Override 60 | protected void initChannel(Channel channel) throws Exception { 61 | ChannelPipeline pipeline = channel.pipeline(); 62 | 63 | pipeline.addLast(new IdleStateHandler(0, 10, 0)); 64 | pipeline.addLast("decoder", decoder); 65 | pipeline.addLast("encoder", encoder); // we don't really read responses... 66 | pipeline.addLast("handler", graphiteHandler); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/outbrain/gruffalo/netty/GraphiteClientPool.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo.netty; 2 | 3 | import java.util.concurrent.atomic.AtomicInteger; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import com.google.common.base.Preconditions; 9 | import com.outbrain.swinfra.metrics.api.MetricFactory; 10 | 11 | import io.netty.channel.ChannelHandler; 12 | import io.netty.channel.EventLoopGroup; 13 | import io.netty.handler.codec.string.StringDecoder; 14 | import io.netty.handler.codec.string.StringEncoder; 15 | 16 | /** 17 | * Time: 8/4/13 2:20 PM 18 | * 19 | * @author Eran Harel 20 | */ 21 | class GraphiteClientPool implements GraphiteClient { 22 | 23 | private static final Logger logger = LoggerFactory.getLogger(GraphiteClientPool.class); 24 | private final GraphiteClient[] pool; 25 | private final AtomicInteger nextIndex = new AtomicInteger(); 26 | 27 | public GraphiteClientPool(final EventLoopGroup eventLoopGroup, final StringDecoder decoder, 28 | final StringEncoder encoder, final Throttler throttler, final int inFlightBatchesHighThreshold, final MetricFactory metricFactory, final String graphiteRelayHosts) { 29 | Preconditions.checkNotNull(graphiteRelayHosts); 30 | Preconditions.checkNotNull(decoder, "decoder must not be null"); 31 | Preconditions.checkNotNull(encoder, "encoder must not be null"); 32 | Preconditions.checkNotNull(eventLoopGroup, "eventLoopGroup must not be null"); 33 | 34 | logger.info("Creating a client pool for [{}]", graphiteRelayHosts); 35 | final String[] hosts = graphiteRelayHosts.trim().split(","); 36 | pool = new GraphiteClient[hosts.length]; 37 | initClients(hosts, eventLoopGroup, decoder, encoder, throttler, inFlightBatchesHighThreshold, metricFactory); 38 | } 39 | 40 | private void initClients(final String[] hosts, final EventLoopGroup eventLoopGroup, final StringDecoder decoder, final StringEncoder encoder, 41 | final Throttler throttler, final int inFlightBatchesHighThreshold, final MetricFactory metricFactory) { 42 | for (int i = 0; i < hosts.length; i++) { 43 | final String[] hostAndPort = hosts[i].split(":"); 44 | final String host = hostAndPort[0]; 45 | final int port = Integer.parseInt(hostAndPort[1]); 46 | 47 | final NettyGraphiteClient client = new NettyGraphiteClient(throttler, inFlightBatchesHighThreshold, metricFactory, hosts[i]); 48 | pool[i] = client; 49 | final ChannelHandler graphiteChannelHandler = new GraphiteChannelInboundHandler(client, hosts[i], throttler); 50 | final GraphiteClientChannelInitializer channelInitializer = new GraphiteClientChannelInitializer(host, port, eventLoopGroup, decoder, encoder, 51 | graphiteChannelHandler); 52 | client.setChannelInitializer(channelInitializer); 53 | } 54 | } 55 | 56 | @Override 57 | public void connect() { 58 | for (final GraphiteClient client : pool) { 59 | client.connect(); 60 | } 61 | } 62 | 63 | @Override 64 | public boolean publishMetrics(final String metrics) { 65 | final int currIndex = nextIndex.getAndIncrement(); 66 | 67 | for(int i = 0; i < pool.length; i++) { 68 | final int currClientIndex = (i + currIndex) % pool.length; 69 | if (pool[currClientIndex].publishMetrics(metrics)) { 70 | return true; 71 | } 72 | } 73 | 74 | return false; 75 | } 76 | 77 | @Override 78 | public void onPushBack() { 79 | // meh 80 | // in the future we may implement some weighted round robin using this input 81 | // currently this is only used by the children to add metrics 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/outbrain/gruffalo/netty/GruffaloProxy.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo.netty; 2 | 3 | import com.google.common.base.Preconditions; 4 | import io.netty.buffer.UnpooledByteBufAllocator; 5 | import io.netty.channel.ChannelFutureListener; 6 | import io.netty.channel.ChannelOption; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.context.support.ClassPathXmlApplicationContext; 10 | 11 | import io.netty.bootstrap.Bootstrap; 12 | import io.netty.bootstrap.ServerBootstrap; 13 | import io.netty.channel.ChannelFuture; 14 | import io.netty.channel.EventLoopGroup; 15 | import io.netty.channel.socket.nio.NioDatagramChannel; 16 | import io.netty.channel.socket.nio.NioServerSocketChannel; 17 | 18 | class GruffaloProxy { 19 | 20 | private static final Logger log = LoggerFactory.getLogger(GruffaloProxy.class); 21 | private final ChannelFuture tcpChannelFuture; 22 | private final ChannelFuture udpChannelFuture; 23 | private final EventLoopGroup eventLoopGroup; 24 | private final Throttler throttler; 25 | 26 | public GruffaloProxy(final EventLoopGroup eventLoopGroup, final TcpServerPipelineFactory tcpServerPipelineFactory, 27 | final UdpServerPipelineFactory udpServerPipelineFactory, final int tcpPort, final int udpPort, final Throttler throttler) throws InterruptedException { 28 | this.throttler = Preconditions.checkNotNull(throttler, "throttler must not be null"); 29 | this.eventLoopGroup = Preconditions.checkNotNull(eventLoopGroup, "eventLoopGroup must not be null"); 30 | tcpChannelFuture = createTcpBootstrap(tcpServerPipelineFactory, tcpPort); 31 | udpChannelFuture = createUdpBootstrap(udpServerPipelineFactory, udpPort); 32 | log.info("Initialization completed"); 33 | } 34 | 35 | public static void main(final String[] args) { 36 | new ClassPathXmlApplicationContext("classpath:applicationContext-GruffaloLib-all.xml"); 37 | } 38 | 39 | private ChannelFuture createUdpBootstrap(final UdpServerPipelineFactory udpServerPipelineFactory, final int udpPort) throws InterruptedException { 40 | log.info("Initializing UDP..."); 41 | Bootstrap udpBootstrap = new Bootstrap(); 42 | udpBootstrap.group(eventLoopGroup).channel(NioDatagramChannel.class).handler(udpServerPipelineFactory); 43 | 44 | final ChannelFuture channelFuture = udpBootstrap.bind(udpPort); 45 | log.info("Binding to UDP port {}", udpPort); 46 | return channelFuture; 47 | } 48 | 49 | private ChannelFuture createTcpBootstrap(final TcpServerPipelineFactory tcpServerPipelineFactory, final int tcpPort) throws InterruptedException { 50 | log.info("Initializing TCP..."); 51 | ServerBootstrap tcpBootstrap = new ServerBootstrap(); 52 | tcpBootstrap.group(eventLoopGroup); 53 | tcpBootstrap.channel(NioServerSocketChannel.class); 54 | tcpBootstrap.childHandler(tcpServerPipelineFactory); 55 | tcpBootstrap.option(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT); 56 | 57 | final ChannelFuture channelFuture = tcpBootstrap.bind(tcpPort).addListener(new ChannelFutureListener() { 58 | @Override 59 | public void operationComplete(final ChannelFuture future) throws Exception { 60 | throttler.setServerChannel(future.channel()); 61 | } 62 | }); 63 | log.info("Binding to TCP port {}", tcpPort); 64 | return channelFuture; 65 | } 66 | 67 | public void shutdown() throws InterruptedException { 68 | log.info("Shutting down inbound channels..."); 69 | tcpChannelFuture.sync().channel().closeFuture().await(200); 70 | udpChannelFuture.sync().channel().closeFuture().await(200); 71 | 72 | log.info("Shutting down server..."); 73 | eventLoopGroup.shutdownGracefully(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/outbrain/gruffalo/netty/LineBasedFrameDecoderFactory.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo.netty; 2 | 3 | import io.netty.handler.codec.LineBasedFrameDecoder; 4 | 5 | public interface LineBasedFrameDecoderFactory { 6 | 7 | public LineBasedFrameDecoder getLineFramer(); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/outbrain/gruffalo/netty/MetricBatcher.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo.netty; 2 | 3 | import java.io.IOException; 4 | import java.net.SocketAddress; 5 | import java.util.concurrent.atomic.AtomicInteger; 6 | 7 | import com.outbrain.swinfra.metrics.api.Histogram; 8 | import org.joda.time.DateTime; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import com.google.common.base.Preconditions; 13 | import com.outbrain.swinfra.metrics.api.Counter; 14 | import com.outbrain.swinfra.metrics.api.Gauge; 15 | import com.outbrain.swinfra.metrics.api.MetricFactory; 16 | 17 | import io.netty.channel.ChannelHandlerContext; 18 | import io.netty.channel.SimpleChannelInboundHandler; 19 | import io.netty.channel.group.ChannelGroup; 20 | import io.netty.handler.timeout.IdleState; 21 | import io.netty.handler.timeout.IdleStateEvent; 22 | 23 | class MetricBatcher extends SimpleChannelInboundHandler { 24 | 25 | private static final Logger log = LoggerFactory.getLogger(MetricBatcher.class); 26 | private static final AtomicInteger lastBatchSize = new AtomicInteger(0); 27 | private final int batchBufferCapacity; 28 | private final Counter connectionCounter; 29 | private final Counter metricsCounter; 30 | private final Counter unexpectedErrorCounter; 31 | private final Counter ioErrorCounter; 32 | private final Counter idleChannelsClosed; 33 | private final ChannelGroup activeChannels; 34 | private final Histogram metricSize; 35 | private final int maxChannelIdleTime; 36 | private StringBuilder batch; 37 | private int currBatchSize; 38 | private DateTime lastRead = DateTime.now(); 39 | 40 | public MetricBatcher(final MetricFactory metricFactory, final int batchBufferCapacity, final ChannelGroup activeChannels, final int maxChannelIdleTime) { 41 | Preconditions.checkArgument(maxChannelIdleTime > 0, "maxChannelIdleTime must be greater than 0"); 42 | this.maxChannelIdleTime = maxChannelIdleTime; 43 | Preconditions.checkNotNull(metricFactory, "metricFactory may not be null"); 44 | this.batchBufferCapacity = batchBufferCapacity; 45 | this.activeChannels = Preconditions.checkNotNull(activeChannels, "activeChannels must not be null"); 46 | prepareNewBatch(); 47 | 48 | final String component = getClass().getSimpleName(); 49 | connectionCounter = metricFactory.createCounter(component, "connections"); 50 | metricsCounter = metricFactory.createCounter(component, "metricsReceived"); 51 | unexpectedErrorCounter = metricFactory.createCounter(component, "unexpectedErrors"); 52 | ioErrorCounter = metricFactory.createCounter(component, "ioErrors"); 53 | idleChannelsClosed = metricFactory.createCounter(component, "idleChannelsClosed"); 54 | metricSize = metricFactory.createHistogram(component, "metricSize", false); 55 | try { 56 | metricFactory.registerGauge(component, "batchSize", new Gauge() { 57 | @Override 58 | public Integer getValue() { 59 | return lastBatchSize.get(); 60 | } 61 | }); 62 | } catch (IllegalArgumentException e) { 63 | // ignore metric already exists 64 | } 65 | } 66 | 67 | @Override 68 | public void channelRead0(final ChannelHandlerContext ctx, final String msg) throws Exception { 69 | lastRead = DateTime.now(); 70 | currBatchSize++; 71 | if (batch.capacity() < batch.length() + msg.length()) { 72 | sendBatch(ctx); 73 | } 74 | 75 | batch.append(msg); 76 | metricsCounter.inc(); 77 | metricSize.update(msg.length()); 78 | } 79 | 80 | private void sendBatch(final ChannelHandlerContext ctx) { 81 | if (0 < batch.length()) { 82 | ctx.fireChannelRead(new Batch(batch, currBatchSize)); 83 | prepareNewBatch(); 84 | } 85 | } 86 | 87 | private void prepareNewBatch() { 88 | batch = new StringBuilder(batchBufferCapacity); 89 | lastBatchSize.set(currBatchSize); 90 | currBatchSize = 0; 91 | } 92 | 93 | @Override 94 | public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) throws Exception { 95 | if (evt instanceof IdleStateEvent) { 96 | final IdleStateEvent e = (IdleStateEvent) evt; 97 | if (e.state() == IdleState.READER_IDLE) { 98 | sendBatch(ctx); 99 | 100 | final SocketAddress remoteAddress = ctx.channel().remoteAddress(); 101 | if (remoteAddress != null) { 102 | if (lastRead.plusSeconds(maxChannelIdleTime).isBefore(System.currentTimeMillis())) { 103 | log.warn("Closing suspected leaked connection: {}", remoteAddress); 104 | idleChannelsClosed.inc(); 105 | ctx.close(); 106 | lastRead = DateTime.now(); 107 | } 108 | } 109 | } 110 | } 111 | } 112 | 113 | @Override 114 | public void channelRegistered(final ChannelHandlerContext ctx) throws Exception { 115 | if (ctx.channel().remoteAddress() != null) { 116 | connectionCounter.inc(); 117 | activeChannels.add(ctx.channel()); 118 | } 119 | } 120 | 121 | @Override 122 | public void channelUnregistered(final ChannelHandlerContext ctx) throws Exception { 123 | connectionCounter.dec(); 124 | try { 125 | sendBatch(ctx); 126 | } catch (final RuntimeException e) { 127 | log.warn("failed to send last batch when closing channel " + ctx.channel().remoteAddress()); 128 | } 129 | } 130 | 131 | @Override 132 | public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) throws Exception { 133 | if (cause instanceof IOException) { 134 | ioErrorCounter.inc(); 135 | log.error("IOException while handling metrics. Remote host =" + ctx.channel().remoteAddress(), cause); 136 | } else { 137 | unexpectedErrorCounter.inc(); 138 | log.error("Unexpected exception while handling metrics. Remote host =" + ctx.channel().remoteAddress(), cause); 139 | } 140 | ctx.close(); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/main/java/com/outbrain/gruffalo/netty/MetricBatcherFactory.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo.netty; 2 | 3 | interface MetricBatcherFactory { 4 | 5 | public MetricBatcher getMetricBatcher(); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/outbrain/gruffalo/netty/MetricPublishHandler.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo.netty; 2 | 3 | import org.springframework.util.Assert; 4 | 5 | import com.outbrain.gruffalo.publish.MetricsPublisher; 6 | import com.outbrain.swinfra.metrics.api.Counter; 7 | import com.outbrain.swinfra.metrics.api.MetricFactory; 8 | import com.outbrain.swinfra.metrics.api.Timer; 9 | 10 | import io.netty.channel.ChannelHandler.Sharable; 11 | import io.netty.channel.ChannelHandlerContext; 12 | import io.netty.channel.SimpleChannelInboundHandler; 13 | 14 | @Sharable 15 | class MetricPublishHandler extends SimpleChannelInboundHandler { 16 | 17 | private final MetricsPublisher publisher; 18 | private final Timer publishTimer; 19 | private final Counter metricsCounter; 20 | 21 | public MetricPublishHandler(final MetricsPublisher publisher, final MetricFactory metricFactory) { 22 | Assert.notNull(publisher, "publisher may not be null"); 23 | Assert.notNull(metricFactory, "metricFactory may not be null"); 24 | this.publisher = publisher; 25 | String component = getClass().getSimpleName(); 26 | publishTimer = metricFactory.createTimer(component, "publishMetricsBatch"); 27 | metricsCounter = metricFactory.createCounter(component, "metricsSent"); 28 | } 29 | 30 | @Override 31 | public void channelRead0(final ChannelHandlerContext ctx, final Batch msg) throws Exception { 32 | Timer.Context timerContext = publishTimer.time(); 33 | try { 34 | publisher.publishMetrics(msg.payload.toString()); 35 | metricsCounter.inc(msg.batchSize); 36 | } finally { 37 | timerContext.stop(); 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/outbrain/gruffalo/netty/NettyGraphiteClient.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo.netty; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.outbrain.swinfra.metrics.api.Gauge; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import com.outbrain.gruffalo.util.HostName2MetricName; 9 | import com.outbrain.swinfra.metrics.api.MetricFactory; 10 | import com.outbrain.swinfra.metrics.api.Counter; 11 | 12 | import io.netty.channel.ChannelFuture; 13 | import io.netty.channel.ChannelFutureListener; 14 | 15 | import java.util.concurrent.atomic.AtomicInteger; 16 | 17 | /** 18 | * Time: 8/4/13 12:30 PM 19 | * 20 | * @author Eran Harel 21 | */ 22 | public class NettyGraphiteClient implements GraphiteClient { 23 | 24 | private static final Logger log = LoggerFactory.getLogger(NettyGraphiteClient.class); 25 | 26 | private final int inFlightBatchesLowThreshold; 27 | private final int inFlightBatchesHighThreshold; 28 | private final Throttler throttler; 29 | private final AtomicInteger inFlightBatches = new AtomicInteger(0); 30 | private final Counter errorCounter; 31 | private final Counter pushBackCounter; 32 | private final Counter reconnectCounter; 33 | private final Counter rejectedCounter; 34 | private final Counter publishedCounter; 35 | private final String host; 36 | private final ChannelFutureListener opListener = new ChannelFutureListener() { 37 | @Override 38 | public void operationComplete(final ChannelFuture future) throws Exception { 39 | final int inFlightBaches = inFlightBatches.decrementAndGet(); 40 | if(inFlightBaches == inFlightBatchesLowThreshold) { 41 | throttler.restoreClientReads(); 42 | } 43 | 44 | if (future.isSuccess()) { 45 | publishedCounter.inc(); 46 | } else { 47 | errorCounter.inc(); 48 | if (log.isDebugEnabled()) { 49 | log.debug("Failed to write to {}: {}", host, future.cause().toString()); 50 | } 51 | } 52 | } 53 | }; 54 | private GraphiteClientChannelInitializer channelInitializer; 55 | private volatile ChannelFuture channelFuture; 56 | 57 | public NettyGraphiteClient(final Throttler throttler, final int inFlightBatchesHighThreshold, final MetricFactory metricFactory, final String host) { 58 | Preconditions.checkArgument(0 < inFlightBatchesHighThreshold); 59 | this.inFlightBatchesHighThreshold = inFlightBatchesHighThreshold; 60 | this.inFlightBatchesLowThreshold = inFlightBatchesHighThreshold / 5; 61 | Preconditions.checkNotNull(metricFactory, "metricFactory must not be null"); 62 | this.throttler = Preconditions.checkNotNull(throttler, "throttler must not be null"); 63 | this.host = host; 64 | final String graphiteCompatibleHostName = HostName2MetricName.graphiteCompatibleHostPortName(host); 65 | errorCounter = metricFactory.createCounter(getClass().getSimpleName(), graphiteCompatibleHostName + ".errors"); 66 | pushBackCounter = metricFactory.createCounter(getClass().getSimpleName(), graphiteCompatibleHostName + ".pushBack"); 67 | reconnectCounter = metricFactory.createCounter(getClass().getSimpleName(), graphiteCompatibleHostName + ".reconnect"); 68 | rejectedCounter = metricFactory.createCounter(getClass().getSimpleName(), graphiteCompatibleHostName + ".rejected"); 69 | publishedCounter = metricFactory.createCounter(getClass().getSimpleName(), graphiteCompatibleHostName + ".published"); 70 | metricFactory.registerGauge(getClass().getSimpleName(), graphiteCompatibleHostName + ".inFlightBatches", new Gauge() { 71 | @Override 72 | public Integer getValue() { 73 | return inFlightBatches.get(); 74 | } 75 | }); 76 | log.info("Client for [{}] initialized", host); 77 | } 78 | 79 | public void setChannelInitializer(final GraphiteClientChannelInitializer channelInitializer) { 80 | this.channelInitializer = channelInitializer; 81 | } 82 | 83 | @Override 84 | public void connect() { 85 | reconnectCounter.inc(); 86 | log.info("Client for [{}] is reconnecting", host); 87 | channelFuture = channelInitializer.connect(); 88 | } 89 | 90 | @Override 91 | public boolean publishMetrics(final String metrics) { 92 | if (channelFuture.isDone()) { 93 | final int numInFlight = inFlightBatches.incrementAndGet(); 94 | if(inFlightBatchesHighThreshold <= numInFlight) { 95 | onPushBack(); 96 | throttler.pushBackClients(); 97 | } 98 | channelFuture.channel().writeAndFlush(metrics).addListener(opListener); 99 | return true; 100 | } else { 101 | rejectedCounter.inc(); 102 | return false; 103 | } 104 | } 105 | 106 | @Override 107 | public void onPushBack() { 108 | pushBackCounter.inc(); 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/com/outbrain/gruffalo/netty/NettyQueuesMetricsInitializer.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo.netty; 2 | 3 | import com.outbrain.swinfra.metrics.api.Gauge; 4 | import com.outbrain.swinfra.metrics.api.MetricFactory; 5 | import io.netty.channel.EventLoopGroup; 6 | import io.netty.util.concurrent.EventExecutor; 7 | import io.netty.util.concurrent.SingleThreadEventExecutor; 8 | 9 | /** 10 | * Registers metrics on Netty's even loop queues 11 | * @author Eran Harel 12 | */ 13 | class NettyQueuesMetricsInitializer { 14 | 15 | public NettyQueuesMetricsInitializer(final MetricFactory metricFactory, final EventLoopGroup elg) { 16 | if (metricFactory == null || elg == null) { 17 | return; 18 | } 19 | 20 | int index = 0; 21 | for (final EventExecutor eventExecutor : elg) { 22 | if (eventExecutor instanceof SingleThreadEventExecutor) { 23 | final SingleThreadEventExecutor singleExecutor = (SingleThreadEventExecutor) eventExecutor; 24 | metricFactory.registerGauge("GruffaloEventLoopGroup", "EventLoop-" + index, new Gauge() { 25 | @Override 26 | public Integer getValue() { 27 | return singleExecutor.pendingTasks(); 28 | } 29 | }); 30 | 31 | index++; 32 | } 33 | } 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /src/main/java/com/outbrain/gruffalo/netty/TcpServerPipelineFactory.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo.netty; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.channel.ChannelInitializer; 5 | import io.netty.channel.ChannelPipeline; 6 | import io.netty.handler.codec.string.StringDecoder; 7 | import io.netty.handler.timeout.IdleStateHandler; 8 | import io.netty.util.concurrent.EventExecutorGroup; 9 | 10 | import org.springframework.util.Assert; 11 | 12 | class TcpServerPipelineFactory extends ChannelInitializer { 13 | 14 | private final int readerIdleTimeSeconds; 15 | private final LineBasedFrameDecoderFactory framerFactory; 16 | private final StringDecoder decoder; 17 | private final MetricBatcherFactory metricBatcherFactory; 18 | private final MetricPublishHandler publishHandler; 19 | private final EventExecutorGroup publishExecutor; 20 | 21 | public TcpServerPipelineFactory(final int readerIdleTimeSeconds, final LineBasedFrameDecoderFactory framerFactory, final StringDecoder decoder, 22 | final MetricBatcherFactory metricBatcherFactory, final MetricPublishHandler publishHandler, final EventExecutorGroup publishExecutor) { 23 | Assert.notNull(framerFactory, "framerFactory, may not be null"); 24 | Assert.notNull(decoder, "decoder may not be null"); 25 | Assert.notNull(metricBatcherFactory, "metricBatcherFactory may not be null"); 26 | Assert.notNull(publishHandler, "publishHandler may not be null"); 27 | Assert.notNull(publishExecutor, "publishExecutor may not be null"); 28 | 29 | this.readerIdleTimeSeconds = readerIdleTimeSeconds; 30 | this.framerFactory = framerFactory; 31 | this.decoder = decoder; 32 | this.metricBatcherFactory = metricBatcherFactory; 33 | this.publishHandler = publishHandler; 34 | this.publishExecutor = publishExecutor; 35 | } 36 | 37 | @Override 38 | protected void initChannel(final Channel channel) throws Exception { 39 | final ChannelPipeline pipeline = channel.pipeline(); 40 | pipeline.addLast("idleStateHandler", new IdleStateHandler(readerIdleTimeSeconds, 0, 0)); 41 | pipeline.addLast("framer", framerFactory.getLineFramer()); 42 | pipeline.addLast("decoder", decoder); 43 | pipeline.addLast("batchHandler", metricBatcherFactory.getMetricBatcher()); 44 | pipeline.addLast("publishHandler", publishHandler); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/outbrain/gruffalo/netty/Throttler.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo.netty; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.outbrain.swinfra.metrics.api.Gauge; 5 | import com.outbrain.swinfra.metrics.api.MetricFactory; 6 | import io.netty.channel.Channel; 7 | import io.netty.channel.group.ChannelGroup; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import java.util.concurrent.atomic.AtomicBoolean; 12 | 13 | /** 14 | * This little fella is in charge of pushing back / restoring inbound traffic when needed 15 | * 16 | * @author Eran Harel 17 | */ 18 | public class Throttler { 19 | 20 | private static final Logger log = LoggerFactory.getLogger(Throttler.class); 21 | 22 | private final ChannelGroup activeServerChannels; 23 | private Channel serverChannel; 24 | 25 | public Throttler(final ChannelGroup activeServerChannels, MetricFactory metricFactory) { 26 | this.activeServerChannels = Preconditions.checkNotNull(activeServerChannels, "activeServerChannels must not be null"); 27 | Preconditions.checkNotNull(metricFactory, "metricFactory must not be null"); 28 | metricFactory.registerGauge(getClass().getSimpleName(), "autoread", new Gauge() { 29 | @Override 30 | public Integer getValue() { 31 | return serverChannel == null || !serverChannel.config().isAutoRead() ? 0 : 1; 32 | } 33 | }); 34 | } 35 | 36 | public void pushBackClients() { 37 | changeServerAutoRead(false); 38 | } 39 | 40 | public void restoreClientReads() { 41 | changeServerAutoRead(true); 42 | } 43 | 44 | public void changeServerAutoRead(final boolean autoread) { 45 | log.debug("Setting server autoread={}", autoread); 46 | 47 | serverChannel.config().setAutoRead(autoread); 48 | for (final Channel activeServerChannel : activeServerChannels) { 49 | activeServerChannel.config().setAutoRead(autoread); 50 | } 51 | } 52 | 53 | void setServerChannel(final Channel serverChannel) { 54 | this.serverChannel = serverChannel; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/outbrain/gruffalo/netty/UdpServerPipelineFactory.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo.netty; 2 | 3 | import org.springframework.util.Assert; 4 | 5 | import io.netty.channel.Channel; 6 | import io.netty.channel.ChannelInitializer; 7 | import io.netty.channel.ChannelPipeline; 8 | import io.netty.handler.timeout.IdleStateHandler; 9 | import io.netty.util.concurrent.EventExecutorGroup; 10 | 11 | class UdpServerPipelineFactory extends ChannelInitializer { 12 | 13 | private final int readerIdleTimeSeconds; 14 | private final MetricBatcherFactory metricBatcherFactory; 15 | private final DatagramPacketToStringDecoder datagramPacketDecoder; 16 | private final MetricPublishHandler publishHandler; 17 | private final EventExecutorGroup publishExecutor; // TODO remove - unused 18 | 19 | public UdpServerPipelineFactory(final int readerIdleTimeSeconds, final DatagramPacketToStringDecoder datagramPacketDecoder, 20 | final MetricBatcherFactory metricBatcherFactory, final MetricPublishHandler publishHandler, final EventExecutorGroup publishExecutor) { 21 | Assert.notNull(datagramPacketDecoder, "datagramPacketDecoder may not be null"); 22 | Assert.notNull(metricBatcherFactory, "metricBatcherFactory may not be null"); 23 | Assert.notNull(publishHandler, "publishHandler may not be null"); 24 | Assert.notNull(publishExecutor, "publishExecutor may not be null"); 25 | 26 | this.readerIdleTimeSeconds = readerIdleTimeSeconds; 27 | this.metricBatcherFactory = metricBatcherFactory; 28 | this.datagramPacketDecoder = datagramPacketDecoder; 29 | this.publishHandler = publishHandler; 30 | this.publishExecutor = publishExecutor; 31 | } 32 | 33 | @Override 34 | protected void initChannel(final Channel channel) throws Exception { 35 | final ChannelPipeline pipeline = channel.pipeline(); 36 | pipeline.addLast("idleStateHandler", new IdleStateHandler(readerIdleTimeSeconds, 0, 0)); 37 | pipeline.addLast("decoder", datagramPacketDecoder); 38 | pipeline.addLast("batchHandler", metricBatcherFactory.getMetricBatcher()); 39 | pipeline.addLast("publishHandler", publishHandler); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/outbrain/gruffalo/publish/CompoundMetricsPublisher.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo.publish; 2 | 3 | import java.util.LinkedList; 4 | import java.util.List; 5 | 6 | /** 7 | * Time: 10/10/13 2:55 PM 8 | * 9 | * @author Eran Harel 10 | */ 11 | class CompoundMetricsPublisher implements MetricsPublisher { 12 | 13 | private final List publishers = new LinkedList(); 14 | 15 | public CompoundMetricsPublisher(final List publishers) { 16 | this.publishers.addAll(publishers); 17 | } 18 | 19 | @Override 20 | public void publishMetrics(final String payload) { 21 | for (final MetricsPublisher publisher : publishers) { 22 | publisher.publishMetrics(payload); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/outbrain/gruffalo/publish/GraphiteMetricsPublisher.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo.publish; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.outbrain.gruffalo.netty.GraphiteClient; 5 | 6 | /** 7 | * Time: 7/28/13 3:16 PM 8 | * 9 | * @author Eran Harel 10 | */ 11 | class GraphiteMetricsPublisher implements MetricsPublisher { 12 | 13 | private final GraphiteClient graphiteClient; 14 | 15 | public GraphiteMetricsPublisher(final GraphiteClient graphiteClient) { 16 | this.graphiteClient = Preconditions.checkNotNull(graphiteClient, "graphiteClient must not be null"); 17 | } 18 | 19 | @Override 20 | public void publishMetrics(final String metrics) { 21 | graphiteClient.publishMetrics(metrics); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/outbrain/gruffalo/publish/MetricsPublisher.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo.publish; 2 | 3 | public interface MetricsPublisher { 4 | 5 | public void publishMetrics(String payload); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/outbrain/gruffalo/publish/TimedMetricsPublisher.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo.publish; 2 | 3 | 4 | import com.outbrain.swinfra.metrics.api.MetricFactory; 5 | import com.outbrain.swinfra.metrics.api.Timer; 6 | 7 | /** 8 | * A thin layer on top of a {@link com.outbrain.gruffalo.publish.MetricsPublisher} adding timing aspect 9 | * 10 | * @author Eran Harel 11 | */ 12 | class TimedMetricsPublisher implements MetricsPublisher { 13 | 14 | private final MetricsPublisher timedDelegate; 15 | private final Timer timer; 16 | 17 | public TimedMetricsPublisher(final MetricsPublisher timedDelegate, final MetricFactory metricFactory, final String publisherName) { 18 | this.timedDelegate = timedDelegate; 19 | timer = metricFactory.createTimer(timedDelegate.getClass().getSimpleName(), publisherName); 20 | } 21 | 22 | @Override 23 | public void publishMetrics(final String payload) { 24 | final Timer.Context timerContext = timer.time(); 25 | try { 26 | timedDelegate.publishMetrics(payload); 27 | } finally { 28 | timerContext.stop(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/outbrain/gruffalo/util/HostName2MetricName.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo.util; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | /** 6 | * Time: 8/13/13 10:00 AM 7 | * 8 | * @author Eran Harel 9 | */ 10 | public class HostName2MetricName { 11 | private static final Pattern REPLACED_CHARS = Pattern.compile("[\\.\\:]"); 12 | 13 | public static String graphiteCompatibleHostPortName(String hostAndPort) { 14 | return REPLACED_CHARS.matcher(hostAndPort).replaceAll("_"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/resources/applicationContext-GruffaloLib-all.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/resources/applicationContext-GruffaloLib-configuration.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/resources/applicationContext-GruffaloLib-domain.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 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 | -------------------------------------------------------------------------------- /src/main/resources/applicationContext-GruffaloLib-netty.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 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 | -------------------------------------------------------------------------------- /src/main/resources/applicationContext-GruffaloLib-standalone-metrics.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main/resources/stand-alone-demo.properties: -------------------------------------------------------------------------------- 1 | # TCP listen port 2 | com.outbrain.gruffalo.tcp.port=3003 3 | # UDP listen port 4 | com.outbrain.gruffalo.udp.port=3004 5 | # Max amount of time (seconds) in which a channel can be idle before we send the current batch 6 | # If this is 0 - this feature is disabled 7 | # If this is > 0 - batches smaller than maxBatchCapacity may be sent for idle or slow channels 8 | com.outbrain.gruffalo.netty.readerIdleTimeSeconds=10 9 | # max size of metrics batch in characters 10 | # this setting affects the memory footprint of the server - each channel holds a buffer of the specified size for batching 11 | com.outbrain.gruffalo.netty.MetricBatcher.batchBufferCapacity=8192 12 | # Maximum length of a frame we're willing to decode. 13 | com.outbrain.gruffalo.netty.maxMetricLength=4096 14 | 15 | com.outbrain.metrics.publisher.bean.name=compoundMetricsPublisher 16 | com.outbrain.metrics.graphite.relay.hosts=localhost:2003,localhost:2004 17 | com.outbrain.metrics.graphite.relay.secondary.hosts=localhost:2005,localhost:2006 18 | 19 | com.outbrain.gruffalo.maxChannelIdleTimeSec=120 20 | com.outbrain.gruffalo.inFlightBatchesHighThreshold=1500 21 | -------------------------------------------------------------------------------- /src/main/resources/stand-alone-log4j.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/test/java/com/outbrain/gruffalo/Benchmark.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo; 2 | 3 | import java.io.BufferedWriter; 4 | import java.io.IOException; 5 | import java.io.OutputStreamWriter; 6 | import java.net.Socket; 7 | import java.util.concurrent.ExecutorService; 8 | import java.util.concurrent.Executors; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | import org.apache.commons.lang3.time.StopWatch; 12 | import org.joda.time.DateTime; 13 | 14 | public class Benchmark { 15 | 16 | /** 17 | * @param args 18 | * @throws IOException 19 | * @throws InterruptedException 20 | */ 21 | public static void main(final String[] args) throws IOException, InterruptedException { 22 | final int nThreads = 8; 23 | final ExecutorService executor = Executors.newFixedThreadPool(nThreads); 24 | 25 | for (int i = 0; i < nThreads; i++) { 26 | executor.execute(new BenchmarkTask()); 27 | } 28 | 29 | executor.awaitTermination(60, TimeUnit.SECONDS); 30 | executor.shutdown(); 31 | } 32 | 33 | private static class BenchmarkTask implements Runnable { 34 | 35 | private Socket socket; 36 | private BufferedWriter writer; 37 | 38 | @Override 39 | public void run() { 40 | connect(); 41 | 42 | final StopWatch time = new StopWatch(); 43 | time.start(); 44 | try { 45 | for (int i = 0; i < 20000000; i++) { 46 | final StringBuilder payload = new StringBuilder(40); 47 | payload.append(i).append(" - ").append(new DateTime()).append('\n'); 48 | try { 49 | writer.write(payload.toString()); 50 | if (i % 10000 == 0) { 51 | writer.flush(); 52 | } 53 | if (i % 250000 == 0) { 54 | Thread.sleep(300); 55 | } 56 | if (i % 500000 == 0) { 57 | System.out.println("sent " + i); 58 | // Thread.sleep(200); 59 | // disconnect(); 60 | // connect(); 61 | } 62 | } catch (final IOException e) { 63 | throw new RuntimeException(e); 64 | } catch (InterruptedException e) { 65 | e.printStackTrace(); 66 | Thread.currentThread().interrupt(); 67 | return; 68 | } 69 | 70 | // payload.delete(0, 40); 71 | } 72 | 73 | try { 74 | writer.flush(); 75 | } catch (final IOException e) { 76 | throw new RuntimeException(e); 77 | } 78 | } finally { 79 | disconnect(); 80 | } 81 | time.stop(); 82 | System.out.println("Total time " + time); 83 | } 84 | 85 | private void connect() { 86 | try { 87 | socket = new Socket("localhost", 3003); 88 | writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); 89 | } catch (final IOException e) { 90 | throw new RuntimeException(e); 91 | } 92 | } 93 | 94 | private void disconnect() { 95 | try { 96 | writer.close(); 97 | socket.close(); 98 | } catch (final IOException e) { 99 | throw new RuntimeException(e); 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/test/java/com/outbrain/gruffalo/GruffaloTestIT.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo; 2 | 3 | public class GruffaloTestIT /*extends AbstractOb1kServerTestBase*/ { 4 | 5 | // private static final Logger log = LoggerFactory.getLogger(GruffaloTestIT.class); 6 | // 7 | // @Test 8 | // public void testSelfTest() throws Exception { 9 | // assertSelfTestOk(); 10 | // } 11 | // 12 | // @Test 13 | // public void testTcpServer() throws Exception { 14 | // final String payload = createPayload(); 15 | // 16 | // final Socket socket = new Socket("localhost", STProperties.getInstance().getInt("com.outbrain.gruffalo.tcp.port")); 17 | // final Writer writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); 18 | // for (int i = 0; i < 100; i++) { 19 | // writer.write(payload); 20 | // } 21 | // 22 | // try { 23 | // writer.close(); 24 | // socket.close(); 25 | // } catch (IOException e) { 26 | // log.error("Failed to close writer / socket", e); 27 | // } 28 | // } 29 | // 30 | // private String createPayload() { 31 | // final char[] payload = new char[1024]; 32 | // Arrays.fill(payload, 'x'); 33 | // payload[payload.length - 1] = '\n'; 34 | // return new String(payload); 35 | // } 36 | // 37 | // @Override 38 | // protected Server buildServer() { 39 | // return new StandaloneGruffaloServer().build(false); 40 | // } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/com/outbrain/gruffalo/UDPBenchmark.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo; 2 | 3 | import java.io.IOException; 4 | import java.net.DatagramPacket; 5 | import java.net.DatagramSocket; 6 | import java.net.InetAddress; 7 | import java.util.concurrent.ExecutorService; 8 | import java.util.concurrent.Executors; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | import org.apache.commons.lang3.time.StopWatch; 12 | import org.joda.time.DateTime; 13 | 14 | public class UDPBenchmark { 15 | 16 | public static void main(final String[] args) throws IOException, InterruptedException { 17 | final int nThreads = 8; 18 | final ExecutorService executor = Executors.newFixedThreadPool(nThreads); 19 | 20 | for (int i = 0; i < nThreads; i++) { 21 | executor.execute(new BenchmarkTask()); 22 | } 23 | 24 | executor.awaitTermination(60, TimeUnit.SECONDS); 25 | executor.shutdown(); 26 | } 27 | 28 | private static class BenchmarkTask implements Runnable { 29 | @Override 30 | public void run() { 31 | DatagramSocket socket; 32 | InetAddress localhost; 33 | try { 34 | socket = new DatagramSocket(); 35 | localhost = InetAddress.getByName("localhost"); 36 | } catch (final IOException e) { 37 | throw new RuntimeException(e); 38 | } 39 | 40 | final StringBuilder payload = new StringBuilder(40); 41 | final StopWatch time = new StopWatch(); 42 | time.start(); 43 | for (int i = 0; i < 2000000; i++) { 44 | payload.append(i).append(" - ").append(new DateTime()).append('\n'); 45 | try { 46 | socket.send(new DatagramPacket(payload.toString().getBytes(), payload.length(), localhost, 2004)); 47 | } catch (final IOException e) { 48 | throw new RuntimeException(e); 49 | } 50 | 51 | payload.delete(0, 40); 52 | // if (i % 1000 == 0) { 53 | // Thread.sleep(100); 54 | // } 55 | } 56 | 57 | time.stop(); 58 | System.out.println("Total time " + time); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/com/outbrain/gruffalo/publish/GraphiteMetricsPublisherTest.java: -------------------------------------------------------------------------------- 1 | package com.outbrain.gruffalo.publish; 2 | 3 | import com.outbrain.gruffalo.netty.GraphiteChannelInboundHandler; 4 | import com.outbrain.gruffalo.netty.GraphiteClientChannelInitializer; 5 | import com.outbrain.gruffalo.netty.NettyGraphiteClient; 6 | import com.outbrain.gruffalo.netty.Throttler; 7 | import com.outbrain.swinfra.metrics.api.MetricFactory; 8 | import com.outbrain.swinfra.metrics.api.Counter; 9 | import io.netty.channel.nio.NioEventLoopGroup; 10 | import io.netty.handler.codec.string.StringDecoder; 11 | import io.netty.handler.codec.string.StringEncoder; 12 | import org.apache.commons.lang3.time.StopWatch; 13 | import org.mockito.Mockito; 14 | 15 | import java.util.concurrent.TimeUnit; 16 | 17 | /** 18 | * Time: 7/29/13 11:27 AM 19 | * 20 | * @author Eran Harel 21 | */ 22 | public class GraphiteMetricsPublisherTest { 23 | 24 | public static void main(String[] args) throws InterruptedException { 25 | 26 | final MetricFactory metricFactoryMock = Mockito.mock(MetricFactory.class); 27 | final Counter counterMock = Mockito.mock(Counter.class); 28 | Mockito.when(metricFactoryMock.createCounter(Mockito.anyString(), Mockito.anyString())).thenReturn(counterMock); 29 | 30 | NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup(2); 31 | final Throttler throttler = Mockito.mock(Throttler.class); 32 | NettyGraphiteClient client = new NettyGraphiteClient(throttler, 1000, metricFactoryMock, "localhost:666"); 33 | String host = "localhost"; 34 | int port = 3003; 35 | GraphiteClientChannelInitializer channelInitializer = new GraphiteClientChannelInitializer(host, port, eventLoopGroup, new StringDecoder(), new StringEncoder(), new GraphiteChannelInboundHandler(client, host + ":" + port, throttler)); 36 | client.setChannelInitializer(channelInitializer); 37 | client.connect(); 38 | 39 | // Thread.sleep(20000); 40 | System.out.println("Begin bombardment..."); 41 | StopWatch time = new StopWatch(); 42 | time.start(); 43 | for (int i = 0; i < 10000000; i++) { 44 | client.publishMetrics(i + " - 0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\n0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\n0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\n0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\n0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\n0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\n0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\n0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\n0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\n0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\n"); 45 | if(i % 10000 == 0) { 46 | Thread.sleep(100); 47 | } 48 | if (i % 100000 == 0) { 49 | System.out.println(i); 50 | Thread.sleep(300); 51 | } 52 | } 53 | time.stop(); 54 | 55 | System.out.println("sent all data: " + time +"; shutting down..."); 56 | 57 | Thread.sleep(1000000); 58 | eventLoopGroup.shutdownGracefully(10, 20, TimeUnit.SECONDS); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/test/resources/log4j.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | --------------------------------------------------------------------------------