From 07454d11d8dc941f76b5a277eb67cc062c782833 Mon Sep 17 00:00:00 2001 From: Mike McLaughlin Date: Mon, 1 Feb 2016 14:52:47 -0800 Subject: [PATCH 01/10] Support DD agent outbound format as well as dstatsd format --- java-lib/pom.xml | 7 +- .../wavefront/ingester/DogStatsDDecoder.java | 81 ++++ .../java/com/wavefront/ingester/Ingester.java | 114 ++--- .../ingester/StringLineIngester.java | 54 +++ .../com/wavefront/ingester/TcpIngester.java | 70 +++ .../com/wavefront/ingester/UdpIngester.java | 62 +++ .../com/wavefront/agent/AbstractAgent.java | 10 + .../wavefront/agent/ChannelStringHandler.java | 3 + .../wavefront/agent/DataDogAgentHandler.java | 425 ++++++++++++++++++ .../wavefront/agent/DogStatsDUDPHandler.java | 134 ++++++ .../com/wavefront/agent/PointHandler.java | 8 +- .../java/com/wavefront/agent/PushAgent.java | 81 +++- 12 files changed, 981 insertions(+), 68 deletions(-) create mode 100644 java-lib/src/main/java/com/wavefront/ingester/DogStatsDDecoder.java create mode 100644 java-lib/src/main/java/com/wavefront/ingester/StringLineIngester.java create mode 100644 java-lib/src/main/java/com/wavefront/ingester/TcpIngester.java create mode 100644 java-lib/src/main/java/com/wavefront/ingester/UdpIngester.java create mode 100644 proxy/src/main/java/com/wavefront/agent/DataDogAgentHandler.java create mode 100644 proxy/src/main/java/com/wavefront/agent/DogStatsDUDPHandler.java diff --git a/java-lib/pom.xml b/java-lib/pom.xml index 1255155ae..1931e057a 100644 --- a/java-lib/pom.xml +++ b/java-lib/pom.xml @@ -84,6 +84,11 @@ io.netty netty-handler + + io.netty + netty-codec-http + 4.0.10.Final + org.antlr antlr4-runtime @@ -135,4 +140,4 @@ - \ No newline at end of file + diff --git a/java-lib/src/main/java/com/wavefront/ingester/DogStatsDDecoder.java b/java-lib/src/main/java/com/wavefront/ingester/DogStatsDDecoder.java new file mode 100644 index 000000000..ffc96986b --- /dev/null +++ b/java-lib/src/main/java/com/wavefront/ingester/DogStatsDDecoder.java @@ -0,0 +1,81 @@ +package com.wavefront.ingester; + +import java.util.logging.Logger; +import java.util.logging.Level; +import java.util.Iterator; +import java.util.List; +import java.util.HashMap; +import java.util.Map; + +import sunnylabs.report.ReportPoint; + +/** + * DogStatsD decoder that takes a string in this format: + * metric.name:value|type|@sample_rate|#tag1:value,tag2 + */ +public class DogStatsDDecoder implements Decoder { + private static final Logger LOG = Logger.getLogger( + DogStatsDDecoder.class.getCanonicalName()); + + /** + * {@inheritDoc} + */ + @Override + public void decodeReportPoints(String msg, List out, String customerId) { + final Map annotations = new HashMap<>(); + final String[] name_metadata = msg.split(":", 2); + if (name_metadata.length != 2) { + // not a valid message + LOG.warning("Unsupported DogStatsD format: '" + msg + "'"); + return; + } + final String[] parts = name_metadata[1].split("|"); + if (parts.length <= 1) { + LOG.warning("Unsupported DogStatsD message: '" + msg + "'"); + return; + } + if (parts[1].charAt(0) != 'g' && parts[1].charAt(0) != 'c') { + LOG.warning("Skipping DogStatsD metric type: '" + parts[1] + "' (" + msg + ")"); + return; + } + + if (parts.length > 2 && parts[3].charAt(0) == '#') { + for (int i = 3; i < parts.length; i++) { + final String[] tag = parts[i].split(":"); + if (tag.length == 2) { + annotations.put(tag[0], tag[1]); + } + } + } + + out.add(ReportPoint.newBuilder() + .setAnnotations(annotations) + .setMetric(name_metadata[0]) + .setValue(parts[0]) + .setTable("datadog") // TODO: what is table? + .setHost(getHostName()).build()); + LOG.warning(out.get(0).toString()); + } + + /** + * {@inheritDoc} + */ + @Override + public void decodeReportPoints(String msg, List out) { + throw new IllegalStateException("No customer ID set for dogstatsd format"); + } + + /** + * Gets the hostname (assumes windows or unix). This code was lifted from + * this SO question: + * http://stackoverflow.com/a/17958246 + */ + private String getHostName() { + if (System.getProperty("os.name").startsWith("Windows")) { + // Windows will always set the 'COMPUTERNAME' variable + return System.getenv("COMPUTERNAME"); + } else { + return System.getenv("HOSTNAME"); + } + } +} diff --git a/java-lib/src/main/java/com/wavefront/ingester/Ingester.java b/java-lib/src/main/java/com/wavefront/ingester/Ingester.java index d9ac413d9..5669da1b4 100644 --- a/java-lib/src/main/java/com/wavefront/ingester/Ingester.java +++ b/java-lib/src/main/java/com/wavefront/ingester/Ingester.java @@ -9,7 +9,10 @@ import javax.annotation.Nullable; +import io.netty.bootstrap.AbstractBootstrap; import io.netty.bootstrap.ServerBootstrap; + +import io.netty.channel.Channel; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandler; @@ -18,8 +21,6 @@ import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.SocketChannel; -import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.LineBasedFrameDecoder; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.timeout.IdleState; @@ -31,18 +32,34 @@ * * @author Clement Pang (clement@wavefront.com). */ -public class Ingester implements Runnable { - - private static final Logger logger = Logger.getLogger(Ingester.class.getCanonicalName()); +public abstract class Ingester implements Runnable { + private static final Logger logger = + Logger.getLogger(Ingester.class.getCanonicalName()); - private static final int CHANNEL_IDLE_TIMEOUT_IN_SECS = (int) TimeUnit.DAYS.toSeconds(1); + /** + * Default number of seconds before the channel idle timeout handler + * closes the connection. + */ + private static final int CHANNEL_IDLE_TIMEOUT_IN_SECS_DEFAULT = + (int)TimeUnit.DAYS.toSeconds(1); + /** + * Additional decoders to add to the pipeline + */ @Nullable - private final List> decoders; - private final ChannelHandler commandHandler; - private final int listeningPort; + private final List> decoders; + + /** + * The ChannelHandler that is handling the message + */ + protected final ChannelHandler commandHandler; + + /** + * The port that this ingester should be listening on + */ + protected final int listeningPort; - public Ingester(List> decoders, + public Ingester(List> decoders, ChannelHandler commandHandler, int port) { this.listeningPort = port; this.commandHandler = commandHandler; @@ -55,52 +72,41 @@ public Ingester(ChannelHandler commandHandler, int port) { this.decoders = null; } - public void run() { - // Configure the server. - ServerBootstrap b = new ServerBootstrap(); - try { - b.group(new NioEventLoopGroup(), new NioEventLoopGroup()) - .channel(NioServerSocketChannel.class) - .option(ChannelOption.SO_BACKLOG, 100) - .localAddress(listeningPort) - .childHandler(new ChannelInitializer() { - @Override - public void initChannel(SocketChannel ch) throws Exception { - ChannelPipeline pipeline = ch.pipeline(); - pipeline.addLast(new LineBasedFrameDecoder(4096, true, true)); - pipeline.addLast(new StringDecoder(Charsets.UTF_8)); - if (decoders != null) { - for (Function handler : decoders) { - pipeline.addLast(handler.apply(ch)); - } - } - // Shared across all reports for proper batching - pipeline.addLast("idleStateHandler", new IdleStateHandler(CHANNEL_IDLE_TIMEOUT_IN_SECS, - 0, 0)); - pipeline.addLast("idleChannelTerminator", new ChannelDuplexHandler() { - @Override - public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { - if (evt instanceof IdleStateEvent) { - if (((IdleStateEvent) evt).state() == IdleState.READER_IDLE) { - logger.warning("terminating connection to graphite client due to inactivity after " + - CHANNEL_IDLE_TIMEOUT_IN_SECS + "s: " + ctx.channel()); - ctx.close(); - } - } - } - }); - pipeline.addLast(commandHandler); + /** + * Adds an idle timeout handler to the given pipeline + * @param pipeline the pipeline to add the idle timeout handler + */ + protected void addIdleTimeoutHandler(final ChannelPipeline pipeline) { + // Shared across all reports for proper batching + pipeline.addLast("idleStateHandler", + new IdleStateHandler(CHANNEL_IDLE_TIMEOUT_IN_SECS_DEFAULT, + 0, 0)); + pipeline.addLast("idleChannelTerminator", new ChannelDuplexHandler() { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, + Object evt) throws Exception { + if (evt instanceof IdleStateEvent) { + if (((IdleStateEvent) evt).state() == IdleState.READER_IDLE) { + logger.warning("terminating connection to graphite client due to inactivity after " + CHANNEL_IDLE_TIMEOUT_IN_SECS_DEFAULT + "s: " + ctx.channel()); + ctx.close(); } - }); - - // Start the server. - ChannelFuture f = b.bind().sync(); + } + } + }); + } - // Wait until the server socket is closed. - f.channel().closeFuture().sync(); - } catch (InterruptedException e) { - // Server was interrupted - e.printStackTrace(); + /** + * Adds additional decoders passed in during construction of this object + * (if not null). + * @param ch the channel and pipeline to add these decoders to + */ + protected void addDecoders(final Channel ch) { + if (decoders != null) { + ChannelPipeline pipeline = ch.pipeline(); + for (Function handler : decoders) { + pipeline.addLast(handler.apply(ch)); + } } } + } diff --git a/java-lib/src/main/java/com/wavefront/ingester/StringLineIngester.java b/java-lib/src/main/java/com/wavefront/ingester/StringLineIngester.java new file mode 100644 index 000000000..0f1310f72 --- /dev/null +++ b/java-lib/src/main/java/com/wavefront/ingester/StringLineIngester.java @@ -0,0 +1,54 @@ +package com.wavefront.ingester; + +import com.google.common.base.Charsets; +import com.google.common.base.Function; + +import java.util.ArrayList; +import java.util.List; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.handler.codec.LineBasedFrameDecoder; +import io.netty.handler.codec.string.StringDecoder; + +/** + * Default Ingester thread that sets up decoders and a command handler to listen for metrics that are string formatted lines on a port. + * + * @author Clement Pang (clement@wavefront.com). + */ +public class StringLineIngester extends TcpIngester { + + public StringLineIngester(List> decoders, + ChannelHandler commandHandler, int port) { + super(createDecoderList(decoders), commandHandler, port); + } + + public StringLineIngester(ChannelHandler commandHandler, int port) { + super(commandHandler, port); + } + + /** + * Returns a copy of the given list plus inserts the 2 decoders needed for + * this specific ingester (LineBasedFrameDecoder and StringDecoder) + * @param decoders the starting list + * @return copy of the provided list with additional decodiers prepended + */ + private static List> createDecoderList(final List> decoders) { + final List> copy = + new ArrayList<>(decoders); + copy.add(0, new Function() { + @Override + public ChannelHandler apply(Channel input) { + return new LineBasedFrameDecoder(4096, true, true); + } + }); + copy.add(1, new Function() { + @Override + public ChannelHandler apply(Channel input) { + return new StringDecoder(Charsets.UTF_8); + } + }); + + return copy; + } +} diff --git a/java-lib/src/main/java/com/wavefront/ingester/TcpIngester.java b/java-lib/src/main/java/com/wavefront/ingester/TcpIngester.java new file mode 100644 index 000000000..b931eff8d --- /dev/null +++ b/java-lib/src/main/java/com/wavefront/ingester/TcpIngester.java @@ -0,0 +1,70 @@ +package com.wavefront.ingester; + +import com.google.common.base.Charsets; +import com.google.common.base.Function; + +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.Channel; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; + +/** + * Ingester thread that sets up decoders and a command handler to listen for metrics on a port. + * + * @author Clement Pang (clement@wavefront.com). + */ +public class TcpIngester extends Ingester { + + private static final Logger logger = + Logger.getLogger(TcpIngester.class.getCanonicalName()); + + public TcpIngester(List> decoders, + ChannelHandler commandHandler, int port) { + super(decoders, commandHandler, port); + } + + public TcpIngester(ChannelHandler commandHandler, int port) { + super(commandHandler, port); + } + + public void run() { + // Configure the server. + ServerBootstrap b = new ServerBootstrap(); + try { + b.group(new NioEventLoopGroup(), new NioEventLoopGroup()) + .channel(NioServerSocketChannel.class) + .option(ChannelOption.SO_BACKLOG, 100) + .localAddress(listeningPort) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + addDecoders(ch); + addIdleTimeoutHandler(pipeline); + pipeline.addLast(commandHandler); + } + }); + + // Start the server. + ChannelFuture f = b.bind().sync(); + + // Wait until the server socket is closed. + f.channel().closeFuture().sync(); + } catch (InterruptedException e) { + logger.log(Level.WARNING, "Interrupted", e); + + // Server was interrupted + e.printStackTrace(); + } + } +} diff --git a/java-lib/src/main/java/com/wavefront/ingester/UdpIngester.java b/java-lib/src/main/java/com/wavefront/ingester/UdpIngester.java new file mode 100644 index 000000000..2114694d7 --- /dev/null +++ b/java-lib/src/main/java/com/wavefront/ingester/UdpIngester.java @@ -0,0 +1,62 @@ +package com.wavefront.ingester; + +import com.google.common.base.Charsets; +import com.google.common.base.Function; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.net.InetSocketAddress; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.Channel; + +/** + * Ingester thread that sets up decoders and a command handler to listen for metrics on a port. + * + * @author Clement Pang (clement@wavefront.com). + */ +public class UdpIngester extends Ingester { + + private static final Logger logger = + Logger.getLogger(UdpIngester.class.getCanonicalName()); + + public UdpIngester(List> decoders, + ChannelHandler commandHandler, int port) { + super(decoders, commandHandler, port); + } + + public UdpIngester(ChannelHandler commandHandler, int port) { + super(commandHandler, port); + } + + public void run() { + // Configure the server. + final NioEventLoopGroup group = new NioEventLoopGroup(); + try { + final Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioDatagramChannel.class) + .option(ChannelOption.SO_BROADCAST, true) + .handler(commandHandler); + + // Start the server. + b.bind(listeningPort).sync().channel().closeFuture().await(); + } catch (InterruptedException e) { + logger.log(Level.WARNING, "Interrupted", e); + + // Server was interrupted + e.printStackTrace(); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/proxy/src/main/java/com/wavefront/agent/AbstractAgent.java b/proxy/src/main/java/com/wavefront/agent/AbstractAgent.java index 254c5ad9c..03f38bd0e 100644 --- a/proxy/src/main/java/com/wavefront/agent/AbstractAgent.java +++ b/proxy/src/main/java/com/wavefront/agent/AbstractAgent.java @@ -63,6 +63,8 @@ public abstract class AbstractAgent { private static final int GRAPHITE_LISTENING_PORT = 2878; private static final int OPENTSDB_LISTENING_PORT = 4242; private static final int HTTP_JSON_LISTENING_PORT = 3878; + private static final int DOGSTATSD_LISTENING_PORT = 8125; + private static final int DATADOG_HTTP_LISTENING_PORT = 8126; @Parameter(names = {"-f", "--file"}, description = "Proxy configuration file") @@ -163,6 +165,14 @@ public abstract class AbstractAgent { @Parameter(names = {"--opentsdbBlacklistRegex"}, description = "Regex pattern (java.util.regex) that opentsdb input lines must NOT match to be accepted") protected String opentsdbBlacklistRegex; + @Parameter(names = {"--datadogPorts"}, description = "Comma-separated list of ports to listen on for DataDog agent " + + "data. Defaults to: " + DATADOG_HTTP_LISTENING_PORT) + protected String datadogAgentPorts = "" + DATADOG_HTTP_LISTENING_PORT; + + @Parameter(names = {"--dogstatsdPorts"}, description = "Comma-separated list of ports to listen on for DataDog DogStatsD " + + "data. Defaults to: " + DOGSTATSD_LISTENING_PORT) + protected String dogstatsdPorts = "" + DOGSTATSD_LISTENING_PORT; + @Parameter(names = {"--splitPushWhenRateLimited"}, description = "Whether to split the push batch size when the push is rejected by Wavefront due to rate limit. Default false.") protected boolean splitPushWhenRateLimited = false; diff --git a/proxy/src/main/java/com/wavefront/agent/ChannelStringHandler.java b/proxy/src/main/java/com/wavefront/agent/ChannelStringHandler.java index 10b39121a..0bbd44971 100644 --- a/proxy/src/main/java/com/wavefront/agent/ChannelStringHandler.java +++ b/proxy/src/main/java/com/wavefront/agent/ChannelStringHandler.java @@ -15,6 +15,7 @@ import java.util.List; import java.util.UUID; import java.util.regex.Pattern; +import java.util.logging.Logger; import javax.annotation.Nullable; @@ -30,6 +31,8 @@ */ @ChannelHandler.Sharable public class ChannelStringHandler extends SimpleChannelInboundHandler { + private static final Logger logger = + Logger.getLogger(ChannelStringHandler.class.getCanonicalName()); private final Decoder decoder; private final List validatedPoints = new ArrayList<>(); diff --git a/proxy/src/main/java/com/wavefront/agent/DataDogAgentHandler.java b/proxy/src/main/java/com/wavefront/agent/DataDogAgentHandler.java new file mode 100644 index 000000000..1a0c26cec --- /dev/null +++ b/proxy/src/main/java/com/wavefront/agent/DataDogAgentHandler.java @@ -0,0 +1,425 @@ +package com.wavefront.agent; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.util.CharsetUtil; + +import static io.netty.handler.codec.http.HttpHeaders.Names.*; + +import java.nio.ByteBuffer; +import java.util.zip.Inflater; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.logging.Logger; +import java.util.logging.Level; +import java.util.Map; +import java.util.Map.Entry; +import java.util.UUID; + +import com.wavefront.agent.api.ForceQueueEnabledAgentAPI; +import sunnylabs.report.ReportPoint; + +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.JsonNode; + +/** + * This class is a netty channel handler for metrics arriving from + * a DataDog agent. This handler operates as a mini HTTP server and returns + * a 200 status code for all requests (unless an exception occurs). + * To use this, change datadog agent configuration(s) to point the dd_url + * value to the Wavefront proxy where this handler is running. + * To use this to send data to both datadog and WF, be sure to put a proxy + * in front of this handler (teeproxy, or similar) and duplicate requests to + * this handler. + * This is based off the netty example HttpSnoopServer found here: + * https://github.com/netty/netty/tree/4.1/example/src/main/java/io/netty/example/http/snoop + * This class was created from the example provided in HttpSnoopServerHandler + * class in the above directory. + */ +@ChannelHandler.Sharable +public class DataDogAgentHandler extends SimpleChannelInboundHandler { + private static final Logger LOG = Logger.getLogger( + DataDogAgentHandler.class.getCanonicalName()); + + /** + * The HTTP request object passed to channelRead0() + */ + private HttpRequest request; + + /** + * The point handler that takes report metrics one data point at a time + * and handles batching and retries, etc + */ + private final PointHandler pointHandler; + + public DataDogAgentHandler(final ForceQueueEnabledAgentAPI agentAPI, + final UUID daemonId, + final int port, + final String prefix, + final String logLevel, + final String validationLevel, + final long millisecondsPerBatch, + final int blockedPointsPerBatch) { + this.pointHandler = new PointHandler(agentAPI, daemonId, port, logLevel, validationLevel, millisecondsPerBatch, blockedPointsPerBatch); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + ctx.flush(); + } + + @Override + protected void channelRead0(io.netty.channel.ChannelHandlerContext ctx, + Object msg) { + if (msg instanceof HttpRequest) { + this.request = (HttpRequest) msg; + LOG.info(String.format("%s %s", request.getMethod(), request.getUri())); + // TODO: anything to do here? + } + + // NOTE: this requires the use of the HttpObjectAggregator in the netty + // pipeline + if (msg instanceof LastHttpContent) { + final HttpContent httpContent = (HttpContent) msg; + LOG.fine(String.format("[%s] CONTENT\n%s", request.getUri(), request)); + final ByteBuf content = httpContent.content(); + final String header = request.headers().get("Content-Encoding"); + final boolean compressed = (header != null && header.equalsIgnoreCase("deflate")); + if (request.getUri().startsWith("/intake")) { + final JsonNode root = parseJson(content, compressed); + handleIntakeRequest(root); + } else if (request.getUri().startsWith("/api/v1/series")) { + final JsonNode root = parseJson(content, compressed); + handleApiSeries(root); + } else if (request.getUri().equals("/")) { // assume point series + final JsonNode root = parseJson(content, compressed); + handleApiSeries(root); + } else { + LOG.warning(String.format("Ignoring %s %s:\n%s", request.getMethod(), request.getUri(), content.toString(CharsetUtil.UTF_8))); + } + + writeResponse(httpContent, ctx); + } + } + + /** + * Decompress the provided string and parse the resulting JSON string + * @param content the HttpContent (compressed JSON) + * @param isCompressed is the contents compressed? + * @return the parsed root node + */ + private JsonNode parseJson(final ByteBuf content, + final boolean isCompressed) { + if (!content.isReadable()) { + LOG.warning(String.format("[%s] Unable to read content. Ignoring", + request.getUri())); + throw new IllegalArgumentException("Unable to read content"); + } + + // get the contents of the HTTP message body as a byte array + final ByteBuffer data = ByteBuffer.allocate(content.readableBytes()); + content.getBytes(0, data); + byte[] jsonBytes = null; + if (isCompressed) { + final byte[] compressed = data.array(); + jsonBytes = new byte[compressed.length * 100]; + if (isCompressed) { + try { + // decompress the message + final Inflater decompressor = new Inflater(); + decompressor.setInput(compressed); + decompressor.inflate(jsonBytes); + } catch (final java.util.zip.DataFormatException e) { + LOG.log(Level.WARNING, "Failed to decompress message", e); + throw new IllegalArgumentException("Unable to decompress message", e); + } + } + } else { + jsonBytes = data.array(); + } + + // decompressed - now parse JSON + final ObjectMapper jsonTree = new ObjectMapper(); + try { + return jsonTree.readTree(jsonBytes); + } catch (final java.io.IOException e) { + LOG.log(Level.WARNING, + String.format("Unable to parse JSON\n%s", jsonBytes), + e); + throw new IllegalArgumentException("Unable to parse JSON", e); + } + } + + /** + * Handles the HTTP request from the datadog agent with the URI: + * /intake/?api_key= + * ASSUMPTION: the content body is zip'd using ZLib and the value is + * a JSON object. + * Given a json message, this will parse and find the metrics and post those + * to the WF server. The JSON is expected to look like this: + * { + * "metrics": [ + * [ + * "system.disk.total", + * 1451409097, + * 497448.0, + * { + * "device_name": "udev", + * "hostname": "mike-ubuntu14", + * "type": "gauge" + * } + * ], + * ... + * } + * Each metric in the metrics array is consider a report point and is + * sent to the WF server using the PointHandler object. The metric array + * element is made up of: + * (0): metric name + * (1): timestamp (epoch seconds) + * (2): value (assuming float for all values) + * (3): tags (including host); all tags are converted to tags except + * hostname which is sent on its own as the source for the point. + * + * In addition to the metric array elements, all top level elements that + * begin with : + * cpu* + * mem* + * are captured and the value is sent. These items are in the form of: + * { + * ... + * "collection_timestamp": 1451409092.995346, + * "cpuGuest": 0.0, + * "cpuIdle": 99.33, + * "cpuStolen": 0.0, + * ... + * "internalHostname": "mike-ubuntu14", + * ... + * } + * The names are retrieved from the JSON key name splitting the key + * on upper case letters and adding a dot between to form a metric name + * like this example: + * "cpuGuest" => "cpu.guest" + * The value comes from the JSON key's value. + * + * @param root root node of the parsed HttpContent body + */ + private void handleIntakeRequest(final JsonNode root) { + // get the hostname used by all of the top level metrics + final String hostName = root.findPath("internalHostname").asText(); + // get the collection timestamp for all the top level metrics + final double ts = root.findPath("collection_timestamp").asDouble(); + + // iterator over all the top level fields and pull out the name/value + // pairs of all items we care about + // { + // "cpuIdle": 0.00, + // "cpuUser": 0.00, + // .... + // } + Iterator> fields = root.getFields(); + while (fields.hasNext()) { + Map.Entry field = fields.next(); + if (field.getKey().startsWith("cpu") || + field.getKey().startsWith("mem")) + { + final ReportPoint point = ReportPoint.newBuilder() + .setMetric(covertKeyToDottedName(field.getKey())) + .setTimestamp((long)ts * 1000) // convert to ms + .setHost(hostName) + .setValue(field.getValue().asDouble()) + .setTable("datadog") // TODO: what is table? + .build(); + LOG.finer("reporting point: " + point); + pointHandler.reportPoint(point, root.toString()); + } + } + + // metrics array items + final JsonNode metrics = root.findPath("metrics"); + for (final JsonNode metric : metrics) { + // pull out the tags and then search for the hostname + // we won't send the hostname as a tag, we'll send that as "source" + // to WF point handler + final JsonNode tags = metric.get(3); + final Map annotations = new HashMap<>(); + JsonNode host = null; + fields = tags.getFields(); + while (fields.hasNext()) { + Map.Entry field = fields.next(); + if (field.getKey().equals("hostname")) { + host = field.getValue(); + } else { + annotations.put(field.getKey(), field.getValue().asText()); + } + } + + // assuming we found a host, then send the details to WF + if (host != null) { + final ReportPoint point = ReportPoint.newBuilder() + .setAnnotations(annotations) + .setMetric(metric.get(0).asText()) + .setTimestamp(metric.get(1).asLong() * 1000) // convert to ms + .setHost(host.asText()) + .setValue(metric.get(2).asDouble()) + .setTable("datadog") // TODO: what is table? + .build(); + LOG.finer("reporting point: " + point); + pointHandler.reportPoint(point, root.toString()); + } + } + } + + /** + * Handles the HTTP request from the datadog agent with the URI: + * /api/v1/series/?api_key= + * JSON is expected to look like : + * { + * "series": [ + * { + * "device_name" : null, + * "host": "", + * "interval": 10.0, + * "metric": "", + * "points": [ + * [ + * 1451950930.0, + * 0 + * ] + * ], + * "tags": null, + * "type": "gauge" + * }, + * ... + * ] + * } + * The point element is made up of: + * (0): timestamp (epoch seconds) + * (1): value (numeric) + * @param root root node of the parsed HttpContent JSON body + */ + private void handleApiSeries(final JsonNode root) { + // ignore everything else and get the "series" array + final JsonNode metrics = root.findPath("series"); + for (final JsonNode metric : metrics) { + // we currently only support: gauge, histogram + final JsonNode type = metric.findPath("type"); + if (!type.asText().equalsIgnoreCase("gauge") && + !type.asText().equalsIgnoreCase("rate")) { + LOG.warning(String.format("Ignoring '%s' metric type (%s)", type.asText(), root.toString())); + continue; + } + String metricName = metric.findPath("metric").asText(); + + // rate types are created from histograms and seem to end + // in "count" which is a little confusing so add ".rate" + // to the end to clarify. + // NOTE: will need to check that all "rate" types are from the + // histogram + if (type.asText().equalsIgnoreCase("rate")) { + metricName += ".rate"; + } + + // grab the tags + final Map annotations = new HashMap<>(); + final JsonNode tags = metric.findPath("tags"); + if (tags.isArray()) { + // assumption: must be an array, values are strings; format: + // name:value + for (final JsonNode tag : tags) { + final String namevalue = tag.asText(); + final String[] parts = namevalue.split(":"); + if (parts.length != 2) { + LOG.warning(String.format("Expected tag to be in format : but got '%s'. Ignoring this tag.", namevalue)); + continue; + } + annotations.put(parts[0], parts[1]); + } + } + + final JsonNode points = metric.findPath("points"); + for (final JsonNode pt : points) { + final ReportPoint point = ReportPoint.newBuilder() + .setAnnotations(annotations) + .setMetric(metricName) + .setTimestamp(pt.get(0).asLong() * 1000) // convert to ms + .setHost(metric.findPath("host").asText()) + .setValue(pt.get(1).asDouble()) + .setTable("datadog") // TODO: what is table? + .build(); + LOG.finer("reporting point: " + point); + pointHandler.reportPoint(point, root.toString()); + } + } + } + + /** + * Writes the response - 200 if everything went ok. + * This is mostly the same as what was found in the snoop example referenced + * above in the class details. + */ + private void writeResponse(final HttpObject current, + final ChannelHandlerContext ctx) { + // Decide whether to close the connection or not. + final boolean keepAlive = HttpHeaders.isKeepAlive(request); + // Build the response object. + final HttpResponseStatus status = current.getDecoderResult().isSuccess() ? HttpResponseStatus.OK : HttpResponseStatus.BAD_REQUEST; + final FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer("", CharsetUtil.UTF_8)); + + response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); + + if (keepAlive) { + // Add 'Content-Length' header only for a keep-alive connection. + response.headers().set(CONTENT_LENGTH, response.content().readableBytes()); + // Add keep alive header as per: + // - http://www.w3.org/Protocols/HTTP/1.1/draft-ietf-http-v11-spec-01.html#Connection + response.headers().set(CONNECTION, HttpHeaders.Values.KEEP_ALIVE); + } + + // Write the response. + LOG.fine("response: " + response.toString()); + ctx.write(response); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + LOG.log(Level.WARNING, "Failed", cause); + // TODO: write 500 response + ctx.close(); + } + + /** + * Convert a key that is camel-case notation to a dotted equivalent. This + * is best described with an example: + * key = "memPhysFree" + * returns "mem.phys.free" + * @param key a camel-case string value + * @return dotted notation with each uppercase containing a dot before + */ + private String covertKeyToDottedName(final String key) { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < key.length(); i++) { + final char c = key.charAt(i); + if (Character.isUpperCase(c)) { + sb.append("."); + sb.append(Character.toLowerCase(c)); + } else { + sb.append(c); + } + } + return sb.toString(); + } +} diff --git a/proxy/src/main/java/com/wavefront/agent/DogStatsDUDPHandler.java b/proxy/src/main/java/com/wavefront/agent/DogStatsDUDPHandler.java new file mode 100644 index 000000000..eccd0deca --- /dev/null +++ b/proxy/src/main/java/com/wavefront/agent/DogStatsDUDPHandler.java @@ -0,0 +1,134 @@ +package com.wavefront.agent; + +import java.util.logging.Logger; +import java.util.logging.Level; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.socket.DatagramPacket; +import io.netty.util.CharsetUtil; +import sunnylabs.report.ReportPoint; +import com.wavefront.agent.api.ForceQueueEnabledAgentAPI; + +/** + * DogStatsD handler that takes a string in this format: + * metric.name:value|type|@sample_rate|#tag1:value,tag2 + * parses and then sends the metric to the wavefront server. + * Currently only 'g' and 'c' metric types are supported (others are ignored) + */ +public class DogStatsDUDPHandler extends SimpleChannelInboundHandler { + private static final Logger LOG = Logger.getLogger( + DogStatsDUDPHandler.class.getCanonicalName()); + + /** + * The point handler that takes report metrics one data point at a time + * and handles batching and retries, etc + */ + private final PointHandler pointHandler; + + /** + * Constructor (matches the other constructors). + */ + public DogStatsDUDPHandler(final ForceQueueEnabledAgentAPI agentAPI, + final UUID daemonId, + final int port, + final String prefix, + final String logLevel, + final String validationLevel, + final long millisecondsPerBatch, + final int blockedPointsPerBatch) { + this.pointHandler = new PointHandler(agentAPI, daemonId, port, logLevel, validationLevel, millisecondsPerBatch, blockedPointsPerBatch); + } + + /** + * {@inheritDoc} + */ + @Override + public void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) throws Exception { + final String msg = packet.content().toString(CharsetUtil.UTF_8); + LOG.info("Received message '" + msg + "'"); + final ReportPoint point = + decodeMessage(msg, packet.sender().getHostName()); + if (point != null) { + LOG.fine("Sending point : " + point.toString()); + pointHandler.reportPoint(point, msg); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + /** + * {@inheritDoc} + */ + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + LOG.log(Level.WARNING, "Caught exception in dogstatsd handler", cause); + // Don't close the channel because we can keep serving requests. + } + + /** + * Decodes a message received. The expected format is: + * metric.name:value|type|@sample_rate|#tag1:value,tag2 + * @param msg the incoming message + * @param source the source/host + * @return the report point generated or null if the message could not be + * parsed or is invalid. + */ + public ReportPoint decodeMessage(final String msg, final String source) { + final Map annotations = new HashMap<>(); + + // split into name and value + metadata and check the message format + final String[] name_metadata = msg.split(":", 2); + if (name_metadata.length != 2) { + // not a valid message + LOG.warning("Unsupported DogStatsD format: '" + msg + "'"); + return null; + } + final String[] parts = name_metadata[1].split("\\|"); + if (parts.length <= 1) { + LOG.warning("Unsupported DogStatsD message: '" + msg + "'"); + return null; + } + + // check the metric type is supported + if (parts[1].charAt(0) != 'g' && parts[1].charAt(0) != 'c') { + LOG.warning("Skipping DogStatsD metric type: '" + parts[1] + "' (" + msg + ")"); + return null; + } + + // skip over the sample rate and find tags + int loc = 1; + if (parts.length > loc+1) { + if (parts[loc].charAt(0) == '@') { + loc++; + } + if (parts.length > loc+1) { + if (parts[2].charAt(0) == '#') { + for (int i = 3; i < parts.length; i++) { + final String[] tag = parts[i].split(":"); + if (tag.length == 2) { + annotations.put(tag[0], tag[1]); + } + } + } + } + } + + return ReportPoint.newBuilder() + .setHost(source) + .setAnnotations(annotations) + .setMetric(name_metadata[0]) + .setValue(Double.parseDouble(parts[0])) + .setTimestamp(System.currentTimeMillis()) + .setTable("datadog") // TODO: what is table? + .build(); + } +} diff --git a/proxy/src/main/java/com/wavefront/agent/PointHandler.java b/proxy/src/main/java/com/wavefront/agent/PointHandler.java index efea32b39..c9c390a7d 100644 --- a/proxy/src/main/java/com/wavefront/agent/PointHandler.java +++ b/proxy/src/main/java/com/wavefront/agent/PointHandler.java @@ -14,6 +14,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; +import java.util.logging.Level; /** * Adds all graphite strings to a working list, and batches them up on a set schedule (100ms) to be @@ -72,20 +73,17 @@ public void reportPoint(ReportPoint point, String debugLine) { if (!charactersAreValid(point.getMetric())) { illegalCharacterPoints.inc(); String errorMessage = port + ": Point metric has illegal character (" + debugLine + ")"; - logger.warning(errorMessage); throw new RuntimeException(errorMessage); } if (!annotationKeysAreValid(point)) { String errorMessage = port + ": Point annotation key has illegal character (" + debugLine + ")"; - logger.warning(errorMessage); throw new RuntimeException(errorMessage); } if (!pointInRange(point)) { outOfRangePointTimes.inc(); String errorMessage = port + ": Point outside of reasonable time frame (" + debugLine + ")"; - logger.warning(errorMessage); throw new RuntimeException(errorMessage); } @@ -94,7 +92,8 @@ public void reportPoint(ReportPoint point, String debugLine) { switch (validationLevel) { case VALIDATION_NUMERIC_ONLY: if (!(pointValue instanceof Long) && !(pointValue instanceof Double)) { - throw new RuntimeException(port + ": Was not long/double object"); + String errorMessage = port + ": Was not long/double object (" + debugLine + ")"; + throw new RuntimeException(errorMessage); } break; } @@ -105,6 +104,7 @@ public void reportPoint(ReportPoint point, String debugLine) { } } catch (Exception e) { + logger.log(Level.WARNING, "Failed to add point", e); if (this.sendDataTask.getBlockedSampleSize() < this.blockedPointsPerBatch) { this.sendDataTask.addBlockedSample(debugLine); } diff --git a/proxy/src/main/java/com/wavefront/agent/PushAgent.java b/proxy/src/main/java/com/wavefront/agent/PushAgent.java index 4d9ae1405..ff5a16459 100644 --- a/proxy/src/main/java/com/wavefront/agent/PushAgent.java +++ b/proxy/src/main/java/com/wavefront/agent/PushAgent.java @@ -8,9 +8,12 @@ import com.wavefront.ingester.GraphiteDecoder; import com.wavefront.ingester.GraphiteHostAnnotator; import com.wavefront.ingester.Ingester; +import com.wavefront.ingester.StringLineIngester; +import com.wavefront.ingester.TcpIngester; +import com.wavefront.ingester.UdpIngester; import com.wavefront.ingester.OpenTSDBDecoder; -import io.netty.channel.ChannelHandler; -import io.netty.channel.socket.SocketChannel; +import com.wavefront.agent.DataDogAgentHandler; +import com.wavefront.agent.DogStatsDUDPHandler; import org.glassfish.jersey.jackson.JacksonFeature; import org.glassfish.jersey.jetty.JettyHttpContainerFactory; import org.glassfish.jersey.server.ResourceConfig; @@ -19,8 +22,16 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.util.ArrayList; import java.util.List; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpRequestDecoder; +import io.netty.handler.codec.http.HttpResponseEncoder; +import io.netty.handler.codec.http.HttpObjectAggregator; + /** * Push-only Agent. * @@ -65,6 +76,22 @@ protected void startListeners() { } } } + if (datadogAgentPorts != null) { + for (final String strPort : datadogAgentPorts.split(",")) { + if (strPort.trim().length() > 0) { + startDataDogAgentListener(strPort); + logger.info("listening on port: " + strPort + " for DataDog agent metrics"); + } + } + } + if (dogstatsdPorts != null) { + for (final String strPort : dogstatsdPorts.split(",")) { + if (strPort.trim().length() > 0) { + startDogStatsDListener(strPort); + logger.info("listening on port: " + strPort + " for DogStatsD metrics"); + } + } + } if (httpJsonPorts != null) { for (String strPort : httpJsonPorts.split(",")) { if (strPort.trim().length() > 0) { @@ -95,7 +122,42 @@ protected void startOpenTsdbListener(String strPort) { agentAPI, agentId, port, prefix, pushLogLevel, pushValidationLevel, pushFlushInterval, pushBlockedSamples, null, opentsdbWhitelistRegex, opentsdbBlacklistRegex); - new Thread(new Ingester(graphiteHandler, port)).start(); + new Thread(new StringLineIngester(graphiteHandler, port)).start(); + } + + protected void startDogStatsDListener(String strPort) { + int port = Integer.parseInt(strPort); + + // Set up a custom graphite handler, with no formatter + ChannelHandler handler = new DogStatsDUDPHandler(agentAPI, agentId, port, prefix, pushLogLevel, pushValidationLevel, pushFlushInterval, pushBlockedSamples); + new Thread(new UdpIngester(handler, port)).start(); + } + + protected void startDataDogAgentListener(String strPort) { + int port = Integer.parseInt(strPort); + // decoders + List> decoders = new ArrayList<>(); + decoders.add(new Function() { + @Override + public ChannelHandler apply(Channel input) { + return new HttpRequestDecoder(); + } + }); + decoders.add(new Function() { + @Override + public ChannelHandler apply(Channel input) { + return new HttpResponseEncoder(); + } + }); + decoders.add(new Function() { + @Override + public ChannelHandler apply(Channel input) { + return new HttpObjectAggregator(1048576); + } + }); + + ChannelHandler handler = new DataDogAgentHandler(agentAPI, agentId, port, prefix, pushLogLevel, pushValidationLevel, pushFlushInterval, pushBlockedSamples); + new Thread(new TcpIngester(decoders, handler, port)).start(); } protected void startGraphiteListener(String strPort, @@ -108,16 +170,17 @@ protected void startGraphiteListener(String strPort, pushBlockedSamples, formatter, whitelistRegex, blacklistRegex); if (formatter == null) { - List> handler = Lists.newArrayList(1); - handler.add(new Function() { + List> handler = Lists.newArrayList(1); + handler.add(new Function() { @Override - public ChannelHandler apply(SocketChannel input) { - return new GraphiteHostAnnotator(input.remoteAddress().getHostName()); + public ChannelHandler apply(Channel input) { + SocketChannel ch = (SocketChannel)input; + return new GraphiteHostAnnotator(ch.remoteAddress().getHostName()); } }); - new Thread(new Ingester(handler, graphiteHandler, port)).start(); + new Thread(new StringLineIngester(handler, graphiteHandler, port)).start(); } else { - new Thread(new Ingester(graphiteHandler, port)).start(); + new Thread(new StringLineIngester(graphiteHandler, port)).start(); } } From 80e9a1c93c9628045d263a7cbc86e2b364cfb04d Mon Sep 17 00:00:00 2001 From: Mike McLaughlin Date: Wed, 17 Feb 2016 15:05:40 -0800 Subject: [PATCH 02/10] look for datadog ports in the configuration file --- proxy/src/main/java/com/wavefront/agent/AbstractAgent.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/proxy/src/main/java/com/wavefront/agent/AbstractAgent.java b/proxy/src/main/java/com/wavefront/agent/AbstractAgent.java index 03f38bd0e..aff8a6c09 100644 --- a/proxy/src/main/java/com/wavefront/agent/AbstractAgent.java +++ b/proxy/src/main/java/com/wavefront/agent/AbstractAgent.java @@ -265,6 +265,8 @@ private void loadListenerConfigurationFile() throws IOException { opentsdbBlacklistRegex = prop.getProperty("opentsdbBlacklistRegex", opentsdbBlacklistRegex); splitPushWhenRateLimited = Boolean.parseBoolean(prop.getProperty("splitPushWhenRateLimited", String.valueOf(splitPushWhenRateLimited))); + datadogAgentPorts = prop.getProperty("datadogPorts", datadogAgentPorts); + dogstatsdPorts = prop.getProperty("dogstatsdPorts", dogstatsdPorts); retryBackoffBaseSeconds = Double.parseDouble(prop.getProperty("retryBackoffBaseSeconds", String.valueOf(retryBackoffBaseSeconds))); logger.warning("Loaded configuration file " + pushConfigFile); From 414a4578692682f63d0d9689fae5dcb07834b068 Mon Sep 17 00:00:00 2001 From: Mike McLaughlin Date: Wed, 17 Feb 2016 15:06:27 -0800 Subject: [PATCH 03/10] remove filter by type for datadog metrics --- .../com/wavefront/agent/DataDogAgentHandler.java | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/proxy/src/main/java/com/wavefront/agent/DataDogAgentHandler.java b/proxy/src/main/java/com/wavefront/agent/DataDogAgentHandler.java index 1a0c26cec..263df370f 100644 --- a/proxy/src/main/java/com/wavefront/agent/DataDogAgentHandler.java +++ b/proxy/src/main/java/com/wavefront/agent/DataDogAgentHandler.java @@ -314,24 +314,8 @@ private void handleApiSeries(final JsonNode root) { // ignore everything else and get the "series" array final JsonNode metrics = root.findPath("series"); for (final JsonNode metric : metrics) { - // we currently only support: gauge, histogram - final JsonNode type = metric.findPath("type"); - if (!type.asText().equalsIgnoreCase("gauge") && - !type.asText().equalsIgnoreCase("rate")) { - LOG.warning(String.format("Ignoring '%s' metric type (%s)", type.asText(), root.toString())); - continue; - } String metricName = metric.findPath("metric").asText(); - // rate types are created from histograms and seem to end - // in "count" which is a little confusing so add ".rate" - // to the end to clarify. - // NOTE: will need to check that all "rate" types are from the - // histogram - if (type.asText().equalsIgnoreCase("rate")) { - metricName += ".rate"; - } - // grab the tags final Map annotations = new HashMap<>(); final JsonNode tags = metric.findPath("tags"); From ed82ce5936a6259a94a8ebd6d15743cd561751f0 Mon Sep 17 00:00:00 2001 From: Mike McLaughlin Date: Mon, 1 Feb 2016 14:52:47 -0800 Subject: [PATCH 04/10] Support DD agent outbound format as well as dstatsd format --- java-lib/pom.xml | 7 +- .../wavefront/ingester/DogStatsDDecoder.java | 81 ++++ .../java/com/wavefront/ingester/Ingester.java | 114 ++--- .../ingester/StringLineIngester.java | 54 +++ .../com/wavefront/ingester/TcpIngester.java | 70 +++ .../com/wavefront/ingester/UdpIngester.java | 62 +++ .../com/wavefront/agent/AbstractAgent.java | 10 + .../wavefront/agent/ChannelStringHandler.java | 3 + .../wavefront/agent/DataDogAgentHandler.java | 425 ++++++++++++++++++ .../wavefront/agent/DogStatsDUDPHandler.java | 134 ++++++ .../com/wavefront/agent/PointHandler.java | 8 +- .../java/com/wavefront/agent/PushAgent.java | 81 +++- 12 files changed, 981 insertions(+), 68 deletions(-) create mode 100644 java-lib/src/main/java/com/wavefront/ingester/DogStatsDDecoder.java create mode 100644 java-lib/src/main/java/com/wavefront/ingester/StringLineIngester.java create mode 100644 java-lib/src/main/java/com/wavefront/ingester/TcpIngester.java create mode 100644 java-lib/src/main/java/com/wavefront/ingester/UdpIngester.java create mode 100644 proxy/src/main/java/com/wavefront/agent/DataDogAgentHandler.java create mode 100644 proxy/src/main/java/com/wavefront/agent/DogStatsDUDPHandler.java diff --git a/java-lib/pom.xml b/java-lib/pom.xml index 1255155ae..1931e057a 100644 --- a/java-lib/pom.xml +++ b/java-lib/pom.xml @@ -84,6 +84,11 @@ io.netty netty-handler + + io.netty + netty-codec-http + 4.0.10.Final + org.antlr antlr4-runtime @@ -135,4 +140,4 @@ - \ No newline at end of file + diff --git a/java-lib/src/main/java/com/wavefront/ingester/DogStatsDDecoder.java b/java-lib/src/main/java/com/wavefront/ingester/DogStatsDDecoder.java new file mode 100644 index 000000000..ffc96986b --- /dev/null +++ b/java-lib/src/main/java/com/wavefront/ingester/DogStatsDDecoder.java @@ -0,0 +1,81 @@ +package com.wavefront.ingester; + +import java.util.logging.Logger; +import java.util.logging.Level; +import java.util.Iterator; +import java.util.List; +import java.util.HashMap; +import java.util.Map; + +import sunnylabs.report.ReportPoint; + +/** + * DogStatsD decoder that takes a string in this format: + * metric.name:value|type|@sample_rate|#tag1:value,tag2 + */ +public class DogStatsDDecoder implements Decoder { + private static final Logger LOG = Logger.getLogger( + DogStatsDDecoder.class.getCanonicalName()); + + /** + * {@inheritDoc} + */ + @Override + public void decodeReportPoints(String msg, List out, String customerId) { + final Map annotations = new HashMap<>(); + final String[] name_metadata = msg.split(":", 2); + if (name_metadata.length != 2) { + // not a valid message + LOG.warning("Unsupported DogStatsD format: '" + msg + "'"); + return; + } + final String[] parts = name_metadata[1].split("|"); + if (parts.length <= 1) { + LOG.warning("Unsupported DogStatsD message: '" + msg + "'"); + return; + } + if (parts[1].charAt(0) != 'g' && parts[1].charAt(0) != 'c') { + LOG.warning("Skipping DogStatsD metric type: '" + parts[1] + "' (" + msg + ")"); + return; + } + + if (parts.length > 2 && parts[3].charAt(0) == '#') { + for (int i = 3; i < parts.length; i++) { + final String[] tag = parts[i].split(":"); + if (tag.length == 2) { + annotations.put(tag[0], tag[1]); + } + } + } + + out.add(ReportPoint.newBuilder() + .setAnnotations(annotations) + .setMetric(name_metadata[0]) + .setValue(parts[0]) + .setTable("datadog") // TODO: what is table? + .setHost(getHostName()).build()); + LOG.warning(out.get(0).toString()); + } + + /** + * {@inheritDoc} + */ + @Override + public void decodeReportPoints(String msg, List out) { + throw new IllegalStateException("No customer ID set for dogstatsd format"); + } + + /** + * Gets the hostname (assumes windows or unix). This code was lifted from + * this SO question: + * http://stackoverflow.com/a/17958246 + */ + private String getHostName() { + if (System.getProperty("os.name").startsWith("Windows")) { + // Windows will always set the 'COMPUTERNAME' variable + return System.getenv("COMPUTERNAME"); + } else { + return System.getenv("HOSTNAME"); + } + } +} diff --git a/java-lib/src/main/java/com/wavefront/ingester/Ingester.java b/java-lib/src/main/java/com/wavefront/ingester/Ingester.java index d9ac413d9..5669da1b4 100644 --- a/java-lib/src/main/java/com/wavefront/ingester/Ingester.java +++ b/java-lib/src/main/java/com/wavefront/ingester/Ingester.java @@ -9,7 +9,10 @@ import javax.annotation.Nullable; +import io.netty.bootstrap.AbstractBootstrap; import io.netty.bootstrap.ServerBootstrap; + +import io.netty.channel.Channel; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandler; @@ -18,8 +21,6 @@ import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.SocketChannel; -import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.LineBasedFrameDecoder; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.timeout.IdleState; @@ -31,18 +32,34 @@ * * @author Clement Pang (clement@wavefront.com). */ -public class Ingester implements Runnable { - - private static final Logger logger = Logger.getLogger(Ingester.class.getCanonicalName()); +public abstract class Ingester implements Runnable { + private static final Logger logger = + Logger.getLogger(Ingester.class.getCanonicalName()); - private static final int CHANNEL_IDLE_TIMEOUT_IN_SECS = (int) TimeUnit.DAYS.toSeconds(1); + /** + * Default number of seconds before the channel idle timeout handler + * closes the connection. + */ + private static final int CHANNEL_IDLE_TIMEOUT_IN_SECS_DEFAULT = + (int)TimeUnit.DAYS.toSeconds(1); + /** + * Additional decoders to add to the pipeline + */ @Nullable - private final List> decoders; - private final ChannelHandler commandHandler; - private final int listeningPort; + private final List> decoders; + + /** + * The ChannelHandler that is handling the message + */ + protected final ChannelHandler commandHandler; + + /** + * The port that this ingester should be listening on + */ + protected final int listeningPort; - public Ingester(List> decoders, + public Ingester(List> decoders, ChannelHandler commandHandler, int port) { this.listeningPort = port; this.commandHandler = commandHandler; @@ -55,52 +72,41 @@ public Ingester(ChannelHandler commandHandler, int port) { this.decoders = null; } - public void run() { - // Configure the server. - ServerBootstrap b = new ServerBootstrap(); - try { - b.group(new NioEventLoopGroup(), new NioEventLoopGroup()) - .channel(NioServerSocketChannel.class) - .option(ChannelOption.SO_BACKLOG, 100) - .localAddress(listeningPort) - .childHandler(new ChannelInitializer() { - @Override - public void initChannel(SocketChannel ch) throws Exception { - ChannelPipeline pipeline = ch.pipeline(); - pipeline.addLast(new LineBasedFrameDecoder(4096, true, true)); - pipeline.addLast(new StringDecoder(Charsets.UTF_8)); - if (decoders != null) { - for (Function handler : decoders) { - pipeline.addLast(handler.apply(ch)); - } - } - // Shared across all reports for proper batching - pipeline.addLast("idleStateHandler", new IdleStateHandler(CHANNEL_IDLE_TIMEOUT_IN_SECS, - 0, 0)); - pipeline.addLast("idleChannelTerminator", new ChannelDuplexHandler() { - @Override - public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { - if (evt instanceof IdleStateEvent) { - if (((IdleStateEvent) evt).state() == IdleState.READER_IDLE) { - logger.warning("terminating connection to graphite client due to inactivity after " + - CHANNEL_IDLE_TIMEOUT_IN_SECS + "s: " + ctx.channel()); - ctx.close(); - } - } - } - }); - pipeline.addLast(commandHandler); + /** + * Adds an idle timeout handler to the given pipeline + * @param pipeline the pipeline to add the idle timeout handler + */ + protected void addIdleTimeoutHandler(final ChannelPipeline pipeline) { + // Shared across all reports for proper batching + pipeline.addLast("idleStateHandler", + new IdleStateHandler(CHANNEL_IDLE_TIMEOUT_IN_SECS_DEFAULT, + 0, 0)); + pipeline.addLast("idleChannelTerminator", new ChannelDuplexHandler() { + @Override + public void userEventTriggered(ChannelHandlerContext ctx, + Object evt) throws Exception { + if (evt instanceof IdleStateEvent) { + if (((IdleStateEvent) evt).state() == IdleState.READER_IDLE) { + logger.warning("terminating connection to graphite client due to inactivity after " + CHANNEL_IDLE_TIMEOUT_IN_SECS_DEFAULT + "s: " + ctx.channel()); + ctx.close(); } - }); - - // Start the server. - ChannelFuture f = b.bind().sync(); + } + } + }); + } - // Wait until the server socket is closed. - f.channel().closeFuture().sync(); - } catch (InterruptedException e) { - // Server was interrupted - e.printStackTrace(); + /** + * Adds additional decoders passed in during construction of this object + * (if not null). + * @param ch the channel and pipeline to add these decoders to + */ + protected void addDecoders(final Channel ch) { + if (decoders != null) { + ChannelPipeline pipeline = ch.pipeline(); + for (Function handler : decoders) { + pipeline.addLast(handler.apply(ch)); + } } } + } diff --git a/java-lib/src/main/java/com/wavefront/ingester/StringLineIngester.java b/java-lib/src/main/java/com/wavefront/ingester/StringLineIngester.java new file mode 100644 index 000000000..0f1310f72 --- /dev/null +++ b/java-lib/src/main/java/com/wavefront/ingester/StringLineIngester.java @@ -0,0 +1,54 @@ +package com.wavefront.ingester; + +import com.google.common.base.Charsets; +import com.google.common.base.Function; + +import java.util.ArrayList; +import java.util.List; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.handler.codec.LineBasedFrameDecoder; +import io.netty.handler.codec.string.StringDecoder; + +/** + * Default Ingester thread that sets up decoders and a command handler to listen for metrics that are string formatted lines on a port. + * + * @author Clement Pang (clement@wavefront.com). + */ +public class StringLineIngester extends TcpIngester { + + public StringLineIngester(List> decoders, + ChannelHandler commandHandler, int port) { + super(createDecoderList(decoders), commandHandler, port); + } + + public StringLineIngester(ChannelHandler commandHandler, int port) { + super(commandHandler, port); + } + + /** + * Returns a copy of the given list plus inserts the 2 decoders needed for + * this specific ingester (LineBasedFrameDecoder and StringDecoder) + * @param decoders the starting list + * @return copy of the provided list with additional decodiers prepended + */ + private static List> createDecoderList(final List> decoders) { + final List> copy = + new ArrayList<>(decoders); + copy.add(0, new Function() { + @Override + public ChannelHandler apply(Channel input) { + return new LineBasedFrameDecoder(4096, true, true); + } + }); + copy.add(1, new Function() { + @Override + public ChannelHandler apply(Channel input) { + return new StringDecoder(Charsets.UTF_8); + } + }); + + return copy; + } +} diff --git a/java-lib/src/main/java/com/wavefront/ingester/TcpIngester.java b/java-lib/src/main/java/com/wavefront/ingester/TcpIngester.java new file mode 100644 index 000000000..b931eff8d --- /dev/null +++ b/java-lib/src/main/java/com/wavefront/ingester/TcpIngester.java @@ -0,0 +1,70 @@ +package com.wavefront.ingester; + +import com.google.common.base.Charsets; +import com.google.common.base.Function; + +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.Channel; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; + +/** + * Ingester thread that sets up decoders and a command handler to listen for metrics on a port. + * + * @author Clement Pang (clement@wavefront.com). + */ +public class TcpIngester extends Ingester { + + private static final Logger logger = + Logger.getLogger(TcpIngester.class.getCanonicalName()); + + public TcpIngester(List> decoders, + ChannelHandler commandHandler, int port) { + super(decoders, commandHandler, port); + } + + public TcpIngester(ChannelHandler commandHandler, int port) { + super(commandHandler, port); + } + + public void run() { + // Configure the server. + ServerBootstrap b = new ServerBootstrap(); + try { + b.group(new NioEventLoopGroup(), new NioEventLoopGroup()) + .channel(NioServerSocketChannel.class) + .option(ChannelOption.SO_BACKLOG, 100) + .localAddress(listeningPort) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline pipeline = ch.pipeline(); + addDecoders(ch); + addIdleTimeoutHandler(pipeline); + pipeline.addLast(commandHandler); + } + }); + + // Start the server. + ChannelFuture f = b.bind().sync(); + + // Wait until the server socket is closed. + f.channel().closeFuture().sync(); + } catch (InterruptedException e) { + logger.log(Level.WARNING, "Interrupted", e); + + // Server was interrupted + e.printStackTrace(); + } + } +} diff --git a/java-lib/src/main/java/com/wavefront/ingester/UdpIngester.java b/java-lib/src/main/java/com/wavefront/ingester/UdpIngester.java new file mode 100644 index 000000000..2114694d7 --- /dev/null +++ b/java-lib/src/main/java/com/wavefront/ingester/UdpIngester.java @@ -0,0 +1,62 @@ +package com.wavefront.ingester; + +import com.google.common.base.Charsets; +import com.google.common.base.Function; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.net.InetSocketAddress; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.Channel; + +/** + * Ingester thread that sets up decoders and a command handler to listen for metrics on a port. + * + * @author Clement Pang (clement@wavefront.com). + */ +public class UdpIngester extends Ingester { + + private static final Logger logger = + Logger.getLogger(UdpIngester.class.getCanonicalName()); + + public UdpIngester(List> decoders, + ChannelHandler commandHandler, int port) { + super(decoders, commandHandler, port); + } + + public UdpIngester(ChannelHandler commandHandler, int port) { + super(commandHandler, port); + } + + public void run() { + // Configure the server. + final NioEventLoopGroup group = new NioEventLoopGroup(); + try { + final Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioDatagramChannel.class) + .option(ChannelOption.SO_BROADCAST, true) + .handler(commandHandler); + + // Start the server. + b.bind(listeningPort).sync().channel().closeFuture().await(); + } catch (InterruptedException e) { + logger.log(Level.WARNING, "Interrupted", e); + + // Server was interrupted + e.printStackTrace(); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/proxy/src/main/java/com/wavefront/agent/AbstractAgent.java b/proxy/src/main/java/com/wavefront/agent/AbstractAgent.java index 2895dc1b7..96a5f60f0 100644 --- a/proxy/src/main/java/com/wavefront/agent/AbstractAgent.java +++ b/proxy/src/main/java/com/wavefront/agent/AbstractAgent.java @@ -64,6 +64,8 @@ public abstract class AbstractAgent { private static final int GRAPHITE_LISTENING_PORT = 2878; private static final int OPENTSDB_LISTENING_PORT = 4242; private static final int HTTP_JSON_LISTENING_PORT = 3878; + private static final int DOGSTATSD_LISTENING_PORT = 8125; + private static final int DATADOG_HTTP_LISTENING_PORT = 8126; @Parameter(names = {"-f", "--file"}, description = "Proxy configuration file") @@ -164,6 +166,14 @@ public abstract class AbstractAgent { @Parameter(names = {"--opentsdbBlacklistRegex"}, description = "Regex pattern (java.util.regex) that opentsdb input lines must NOT match to be accepted") protected String opentsdbBlacklistRegex; + @Parameter(names = {"--datadogPorts"}, description = "Comma-separated list of ports to listen on for DataDog agent " + + "data. Defaults to: " + DATADOG_HTTP_LISTENING_PORT) + protected String datadogAgentPorts = "" + DATADOG_HTTP_LISTENING_PORT; + + @Parameter(names = {"--dogstatsdPorts"}, description = "Comma-separated list of ports to listen on for DataDog DogStatsD " + + "data. Defaults to: " + DOGSTATSD_LISTENING_PORT) + protected String dogstatsdPorts = "" + DOGSTATSD_LISTENING_PORT; + @Parameter(names = {"--splitPushWhenRateLimited"}, description = "Whether to split the push batch size when the push is rejected by Wavefront due to rate limit. Default false.") protected boolean splitPushWhenRateLimited = false; diff --git a/proxy/src/main/java/com/wavefront/agent/ChannelStringHandler.java b/proxy/src/main/java/com/wavefront/agent/ChannelStringHandler.java index 10b39121a..0bbd44971 100644 --- a/proxy/src/main/java/com/wavefront/agent/ChannelStringHandler.java +++ b/proxy/src/main/java/com/wavefront/agent/ChannelStringHandler.java @@ -15,6 +15,7 @@ import java.util.List; import java.util.UUID; import java.util.regex.Pattern; +import java.util.logging.Logger; import javax.annotation.Nullable; @@ -30,6 +31,8 @@ */ @ChannelHandler.Sharable public class ChannelStringHandler extends SimpleChannelInboundHandler { + private static final Logger logger = + Logger.getLogger(ChannelStringHandler.class.getCanonicalName()); private final Decoder decoder; private final List validatedPoints = new ArrayList<>(); diff --git a/proxy/src/main/java/com/wavefront/agent/DataDogAgentHandler.java b/proxy/src/main/java/com/wavefront/agent/DataDogAgentHandler.java new file mode 100644 index 000000000..1a0c26cec --- /dev/null +++ b/proxy/src/main/java/com/wavefront/agent/DataDogAgentHandler.java @@ -0,0 +1,425 @@ +package com.wavefront.agent; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.util.CharsetUtil; + +import static io.netty.handler.codec.http.HttpHeaders.Names.*; + +import java.nio.ByteBuffer; +import java.util.zip.Inflater; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.logging.Logger; +import java.util.logging.Level; +import java.util.Map; +import java.util.Map.Entry; +import java.util.UUID; + +import com.wavefront.agent.api.ForceQueueEnabledAgentAPI; +import sunnylabs.report.ReportPoint; + +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.JsonNode; + +/** + * This class is a netty channel handler for metrics arriving from + * a DataDog agent. This handler operates as a mini HTTP server and returns + * a 200 status code for all requests (unless an exception occurs). + * To use this, change datadog agent configuration(s) to point the dd_url + * value to the Wavefront proxy where this handler is running. + * To use this to send data to both datadog and WF, be sure to put a proxy + * in front of this handler (teeproxy, or similar) and duplicate requests to + * this handler. + * This is based off the netty example HttpSnoopServer found here: + * https://github.com/netty/netty/tree/4.1/example/src/main/java/io/netty/example/http/snoop + * This class was created from the example provided in HttpSnoopServerHandler + * class in the above directory. + */ +@ChannelHandler.Sharable +public class DataDogAgentHandler extends SimpleChannelInboundHandler { + private static final Logger LOG = Logger.getLogger( + DataDogAgentHandler.class.getCanonicalName()); + + /** + * The HTTP request object passed to channelRead0() + */ + private HttpRequest request; + + /** + * The point handler that takes report metrics one data point at a time + * and handles batching and retries, etc + */ + private final PointHandler pointHandler; + + public DataDogAgentHandler(final ForceQueueEnabledAgentAPI agentAPI, + final UUID daemonId, + final int port, + final String prefix, + final String logLevel, + final String validationLevel, + final long millisecondsPerBatch, + final int blockedPointsPerBatch) { + this.pointHandler = new PointHandler(agentAPI, daemonId, port, logLevel, validationLevel, millisecondsPerBatch, blockedPointsPerBatch); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + ctx.flush(); + } + + @Override + protected void channelRead0(io.netty.channel.ChannelHandlerContext ctx, + Object msg) { + if (msg instanceof HttpRequest) { + this.request = (HttpRequest) msg; + LOG.info(String.format("%s %s", request.getMethod(), request.getUri())); + // TODO: anything to do here? + } + + // NOTE: this requires the use of the HttpObjectAggregator in the netty + // pipeline + if (msg instanceof LastHttpContent) { + final HttpContent httpContent = (HttpContent) msg; + LOG.fine(String.format("[%s] CONTENT\n%s", request.getUri(), request)); + final ByteBuf content = httpContent.content(); + final String header = request.headers().get("Content-Encoding"); + final boolean compressed = (header != null && header.equalsIgnoreCase("deflate")); + if (request.getUri().startsWith("/intake")) { + final JsonNode root = parseJson(content, compressed); + handleIntakeRequest(root); + } else if (request.getUri().startsWith("/api/v1/series")) { + final JsonNode root = parseJson(content, compressed); + handleApiSeries(root); + } else if (request.getUri().equals("/")) { // assume point series + final JsonNode root = parseJson(content, compressed); + handleApiSeries(root); + } else { + LOG.warning(String.format("Ignoring %s %s:\n%s", request.getMethod(), request.getUri(), content.toString(CharsetUtil.UTF_8))); + } + + writeResponse(httpContent, ctx); + } + } + + /** + * Decompress the provided string and parse the resulting JSON string + * @param content the HttpContent (compressed JSON) + * @param isCompressed is the contents compressed? + * @return the parsed root node + */ + private JsonNode parseJson(final ByteBuf content, + final boolean isCompressed) { + if (!content.isReadable()) { + LOG.warning(String.format("[%s] Unable to read content. Ignoring", + request.getUri())); + throw new IllegalArgumentException("Unable to read content"); + } + + // get the contents of the HTTP message body as a byte array + final ByteBuffer data = ByteBuffer.allocate(content.readableBytes()); + content.getBytes(0, data); + byte[] jsonBytes = null; + if (isCompressed) { + final byte[] compressed = data.array(); + jsonBytes = new byte[compressed.length * 100]; + if (isCompressed) { + try { + // decompress the message + final Inflater decompressor = new Inflater(); + decompressor.setInput(compressed); + decompressor.inflate(jsonBytes); + } catch (final java.util.zip.DataFormatException e) { + LOG.log(Level.WARNING, "Failed to decompress message", e); + throw new IllegalArgumentException("Unable to decompress message", e); + } + } + } else { + jsonBytes = data.array(); + } + + // decompressed - now parse JSON + final ObjectMapper jsonTree = new ObjectMapper(); + try { + return jsonTree.readTree(jsonBytes); + } catch (final java.io.IOException e) { + LOG.log(Level.WARNING, + String.format("Unable to parse JSON\n%s", jsonBytes), + e); + throw new IllegalArgumentException("Unable to parse JSON", e); + } + } + + /** + * Handles the HTTP request from the datadog agent with the URI: + * /intake/?api_key= + * ASSUMPTION: the content body is zip'd using ZLib and the value is + * a JSON object. + * Given a json message, this will parse and find the metrics and post those + * to the WF server. The JSON is expected to look like this: + * { + * "metrics": [ + * [ + * "system.disk.total", + * 1451409097, + * 497448.0, + * { + * "device_name": "udev", + * "hostname": "mike-ubuntu14", + * "type": "gauge" + * } + * ], + * ... + * } + * Each metric in the metrics array is consider a report point and is + * sent to the WF server using the PointHandler object. The metric array + * element is made up of: + * (0): metric name + * (1): timestamp (epoch seconds) + * (2): value (assuming float for all values) + * (3): tags (including host); all tags are converted to tags except + * hostname which is sent on its own as the source for the point. + * + * In addition to the metric array elements, all top level elements that + * begin with : + * cpu* + * mem* + * are captured and the value is sent. These items are in the form of: + * { + * ... + * "collection_timestamp": 1451409092.995346, + * "cpuGuest": 0.0, + * "cpuIdle": 99.33, + * "cpuStolen": 0.0, + * ... + * "internalHostname": "mike-ubuntu14", + * ... + * } + * The names are retrieved from the JSON key name splitting the key + * on upper case letters and adding a dot between to form a metric name + * like this example: + * "cpuGuest" => "cpu.guest" + * The value comes from the JSON key's value. + * + * @param root root node of the parsed HttpContent body + */ + private void handleIntakeRequest(final JsonNode root) { + // get the hostname used by all of the top level metrics + final String hostName = root.findPath("internalHostname").asText(); + // get the collection timestamp for all the top level metrics + final double ts = root.findPath("collection_timestamp").asDouble(); + + // iterator over all the top level fields and pull out the name/value + // pairs of all items we care about + // { + // "cpuIdle": 0.00, + // "cpuUser": 0.00, + // .... + // } + Iterator> fields = root.getFields(); + while (fields.hasNext()) { + Map.Entry field = fields.next(); + if (field.getKey().startsWith("cpu") || + field.getKey().startsWith("mem")) + { + final ReportPoint point = ReportPoint.newBuilder() + .setMetric(covertKeyToDottedName(field.getKey())) + .setTimestamp((long)ts * 1000) // convert to ms + .setHost(hostName) + .setValue(field.getValue().asDouble()) + .setTable("datadog") // TODO: what is table? + .build(); + LOG.finer("reporting point: " + point); + pointHandler.reportPoint(point, root.toString()); + } + } + + // metrics array items + final JsonNode metrics = root.findPath("metrics"); + for (final JsonNode metric : metrics) { + // pull out the tags and then search for the hostname + // we won't send the hostname as a tag, we'll send that as "source" + // to WF point handler + final JsonNode tags = metric.get(3); + final Map annotations = new HashMap<>(); + JsonNode host = null; + fields = tags.getFields(); + while (fields.hasNext()) { + Map.Entry field = fields.next(); + if (field.getKey().equals("hostname")) { + host = field.getValue(); + } else { + annotations.put(field.getKey(), field.getValue().asText()); + } + } + + // assuming we found a host, then send the details to WF + if (host != null) { + final ReportPoint point = ReportPoint.newBuilder() + .setAnnotations(annotations) + .setMetric(metric.get(0).asText()) + .setTimestamp(metric.get(1).asLong() * 1000) // convert to ms + .setHost(host.asText()) + .setValue(metric.get(2).asDouble()) + .setTable("datadog") // TODO: what is table? + .build(); + LOG.finer("reporting point: " + point); + pointHandler.reportPoint(point, root.toString()); + } + } + } + + /** + * Handles the HTTP request from the datadog agent with the URI: + * /api/v1/series/?api_key= + * JSON is expected to look like : + * { + * "series": [ + * { + * "device_name" : null, + * "host": "", + * "interval": 10.0, + * "metric": "", + * "points": [ + * [ + * 1451950930.0, + * 0 + * ] + * ], + * "tags": null, + * "type": "gauge" + * }, + * ... + * ] + * } + * The point element is made up of: + * (0): timestamp (epoch seconds) + * (1): value (numeric) + * @param root root node of the parsed HttpContent JSON body + */ + private void handleApiSeries(final JsonNode root) { + // ignore everything else and get the "series" array + final JsonNode metrics = root.findPath("series"); + for (final JsonNode metric : metrics) { + // we currently only support: gauge, histogram + final JsonNode type = metric.findPath("type"); + if (!type.asText().equalsIgnoreCase("gauge") && + !type.asText().equalsIgnoreCase("rate")) { + LOG.warning(String.format("Ignoring '%s' metric type (%s)", type.asText(), root.toString())); + continue; + } + String metricName = metric.findPath("metric").asText(); + + // rate types are created from histograms and seem to end + // in "count" which is a little confusing so add ".rate" + // to the end to clarify. + // NOTE: will need to check that all "rate" types are from the + // histogram + if (type.asText().equalsIgnoreCase("rate")) { + metricName += ".rate"; + } + + // grab the tags + final Map annotations = new HashMap<>(); + final JsonNode tags = metric.findPath("tags"); + if (tags.isArray()) { + // assumption: must be an array, values are strings; format: + // name:value + for (final JsonNode tag : tags) { + final String namevalue = tag.asText(); + final String[] parts = namevalue.split(":"); + if (parts.length != 2) { + LOG.warning(String.format("Expected tag to be in format : but got '%s'. Ignoring this tag.", namevalue)); + continue; + } + annotations.put(parts[0], parts[1]); + } + } + + final JsonNode points = metric.findPath("points"); + for (final JsonNode pt : points) { + final ReportPoint point = ReportPoint.newBuilder() + .setAnnotations(annotations) + .setMetric(metricName) + .setTimestamp(pt.get(0).asLong() * 1000) // convert to ms + .setHost(metric.findPath("host").asText()) + .setValue(pt.get(1).asDouble()) + .setTable("datadog") // TODO: what is table? + .build(); + LOG.finer("reporting point: " + point); + pointHandler.reportPoint(point, root.toString()); + } + } + } + + /** + * Writes the response - 200 if everything went ok. + * This is mostly the same as what was found in the snoop example referenced + * above in the class details. + */ + private void writeResponse(final HttpObject current, + final ChannelHandlerContext ctx) { + // Decide whether to close the connection or not. + final boolean keepAlive = HttpHeaders.isKeepAlive(request); + // Build the response object. + final HttpResponseStatus status = current.getDecoderResult().isSuccess() ? HttpResponseStatus.OK : HttpResponseStatus.BAD_REQUEST; + final FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer("", CharsetUtil.UTF_8)); + + response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); + + if (keepAlive) { + // Add 'Content-Length' header only for a keep-alive connection. + response.headers().set(CONTENT_LENGTH, response.content().readableBytes()); + // Add keep alive header as per: + // - http://www.w3.org/Protocols/HTTP/1.1/draft-ietf-http-v11-spec-01.html#Connection + response.headers().set(CONNECTION, HttpHeaders.Values.KEEP_ALIVE); + } + + // Write the response. + LOG.fine("response: " + response.toString()); + ctx.write(response); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + LOG.log(Level.WARNING, "Failed", cause); + // TODO: write 500 response + ctx.close(); + } + + /** + * Convert a key that is camel-case notation to a dotted equivalent. This + * is best described with an example: + * key = "memPhysFree" + * returns "mem.phys.free" + * @param key a camel-case string value + * @return dotted notation with each uppercase containing a dot before + */ + private String covertKeyToDottedName(final String key) { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < key.length(); i++) { + final char c = key.charAt(i); + if (Character.isUpperCase(c)) { + sb.append("."); + sb.append(Character.toLowerCase(c)); + } else { + sb.append(c); + } + } + return sb.toString(); + } +} diff --git a/proxy/src/main/java/com/wavefront/agent/DogStatsDUDPHandler.java b/proxy/src/main/java/com/wavefront/agent/DogStatsDUDPHandler.java new file mode 100644 index 000000000..eccd0deca --- /dev/null +++ b/proxy/src/main/java/com/wavefront/agent/DogStatsDUDPHandler.java @@ -0,0 +1,134 @@ +package com.wavefront.agent; + +import java.util.logging.Logger; +import java.util.logging.Level; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.socket.DatagramPacket; +import io.netty.util.CharsetUtil; +import sunnylabs.report.ReportPoint; +import com.wavefront.agent.api.ForceQueueEnabledAgentAPI; + +/** + * DogStatsD handler that takes a string in this format: + * metric.name:value|type|@sample_rate|#tag1:value,tag2 + * parses and then sends the metric to the wavefront server. + * Currently only 'g' and 'c' metric types are supported (others are ignored) + */ +public class DogStatsDUDPHandler extends SimpleChannelInboundHandler { + private static final Logger LOG = Logger.getLogger( + DogStatsDUDPHandler.class.getCanonicalName()); + + /** + * The point handler that takes report metrics one data point at a time + * and handles batching and retries, etc + */ + private final PointHandler pointHandler; + + /** + * Constructor (matches the other constructors). + */ + public DogStatsDUDPHandler(final ForceQueueEnabledAgentAPI agentAPI, + final UUID daemonId, + final int port, + final String prefix, + final String logLevel, + final String validationLevel, + final long millisecondsPerBatch, + final int blockedPointsPerBatch) { + this.pointHandler = new PointHandler(agentAPI, daemonId, port, logLevel, validationLevel, millisecondsPerBatch, blockedPointsPerBatch); + } + + /** + * {@inheritDoc} + */ + @Override + public void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) throws Exception { + final String msg = packet.content().toString(CharsetUtil.UTF_8); + LOG.info("Received message '" + msg + "'"); + final ReportPoint point = + decodeMessage(msg, packet.sender().getHostName()); + if (point != null) { + LOG.fine("Sending point : " + point.toString()); + pointHandler.reportPoint(point, msg); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + /** + * {@inheritDoc} + */ + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + LOG.log(Level.WARNING, "Caught exception in dogstatsd handler", cause); + // Don't close the channel because we can keep serving requests. + } + + /** + * Decodes a message received. The expected format is: + * metric.name:value|type|@sample_rate|#tag1:value,tag2 + * @param msg the incoming message + * @param source the source/host + * @return the report point generated or null if the message could not be + * parsed or is invalid. + */ + public ReportPoint decodeMessage(final String msg, final String source) { + final Map annotations = new HashMap<>(); + + // split into name and value + metadata and check the message format + final String[] name_metadata = msg.split(":", 2); + if (name_metadata.length != 2) { + // not a valid message + LOG.warning("Unsupported DogStatsD format: '" + msg + "'"); + return null; + } + final String[] parts = name_metadata[1].split("\\|"); + if (parts.length <= 1) { + LOG.warning("Unsupported DogStatsD message: '" + msg + "'"); + return null; + } + + // check the metric type is supported + if (parts[1].charAt(0) != 'g' && parts[1].charAt(0) != 'c') { + LOG.warning("Skipping DogStatsD metric type: '" + parts[1] + "' (" + msg + ")"); + return null; + } + + // skip over the sample rate and find tags + int loc = 1; + if (parts.length > loc+1) { + if (parts[loc].charAt(0) == '@') { + loc++; + } + if (parts.length > loc+1) { + if (parts[2].charAt(0) == '#') { + for (int i = 3; i < parts.length; i++) { + final String[] tag = parts[i].split(":"); + if (tag.length == 2) { + annotations.put(tag[0], tag[1]); + } + } + } + } + } + + return ReportPoint.newBuilder() + .setHost(source) + .setAnnotations(annotations) + .setMetric(name_metadata[0]) + .setValue(Double.parseDouble(parts[0])) + .setTimestamp(System.currentTimeMillis()) + .setTable("datadog") // TODO: what is table? + .build(); + } +} diff --git a/proxy/src/main/java/com/wavefront/agent/PointHandler.java b/proxy/src/main/java/com/wavefront/agent/PointHandler.java index efea32b39..c9c390a7d 100644 --- a/proxy/src/main/java/com/wavefront/agent/PointHandler.java +++ b/proxy/src/main/java/com/wavefront/agent/PointHandler.java @@ -14,6 +14,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; +import java.util.logging.Level; /** * Adds all graphite strings to a working list, and batches them up on a set schedule (100ms) to be @@ -72,20 +73,17 @@ public void reportPoint(ReportPoint point, String debugLine) { if (!charactersAreValid(point.getMetric())) { illegalCharacterPoints.inc(); String errorMessage = port + ": Point metric has illegal character (" + debugLine + ")"; - logger.warning(errorMessage); throw new RuntimeException(errorMessage); } if (!annotationKeysAreValid(point)) { String errorMessage = port + ": Point annotation key has illegal character (" + debugLine + ")"; - logger.warning(errorMessage); throw new RuntimeException(errorMessage); } if (!pointInRange(point)) { outOfRangePointTimes.inc(); String errorMessage = port + ": Point outside of reasonable time frame (" + debugLine + ")"; - logger.warning(errorMessage); throw new RuntimeException(errorMessage); } @@ -94,7 +92,8 @@ public void reportPoint(ReportPoint point, String debugLine) { switch (validationLevel) { case VALIDATION_NUMERIC_ONLY: if (!(pointValue instanceof Long) && !(pointValue instanceof Double)) { - throw new RuntimeException(port + ": Was not long/double object"); + String errorMessage = port + ": Was not long/double object (" + debugLine + ")"; + throw new RuntimeException(errorMessage); } break; } @@ -105,6 +104,7 @@ public void reportPoint(ReportPoint point, String debugLine) { } } catch (Exception e) { + logger.log(Level.WARNING, "Failed to add point", e); if (this.sendDataTask.getBlockedSampleSize() < this.blockedPointsPerBatch) { this.sendDataTask.addBlockedSample(debugLine); } diff --git a/proxy/src/main/java/com/wavefront/agent/PushAgent.java b/proxy/src/main/java/com/wavefront/agent/PushAgent.java index 09d4c99c1..0820e531d 100644 --- a/proxy/src/main/java/com/wavefront/agent/PushAgent.java +++ b/proxy/src/main/java/com/wavefront/agent/PushAgent.java @@ -8,9 +8,12 @@ import com.wavefront.ingester.GraphiteDecoder; import com.wavefront.ingester.GraphiteHostAnnotator; import com.wavefront.ingester.Ingester; +import com.wavefront.ingester.StringLineIngester; +import com.wavefront.ingester.TcpIngester; +import com.wavefront.ingester.UdpIngester; import com.wavefront.ingester.OpenTSDBDecoder; -import io.netty.channel.ChannelHandler; -import io.netty.channel.socket.SocketChannel; +import com.wavefront.agent.DataDogAgentHandler; +import com.wavefront.agent.DogStatsDUDPHandler; import org.glassfish.jersey.jackson.JacksonFeature; import org.glassfish.jersey.jetty.JettyHttpContainerFactory; import org.glassfish.jersey.server.ResourceConfig; @@ -19,8 +22,16 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.util.ArrayList; import java.util.List; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpRequestDecoder; +import io.netty.handler.codec.http.HttpResponseEncoder; +import io.netty.handler.codec.http.HttpObjectAggregator; + /** * Push-only Agent. * @@ -65,6 +76,22 @@ protected void startListeners() { } } } + if (datadogAgentPorts != null) { + for (final String strPort : datadogAgentPorts.split(",")) { + if (strPort.trim().length() > 0) { + startDataDogAgentListener(strPort); + logger.info("listening on port: " + strPort + " for DataDog agent metrics"); + } + } + } + if (dogstatsdPorts != null) { + for (final String strPort : dogstatsdPorts.split(",")) { + if (strPort.trim().length() > 0) { + startDogStatsDListener(strPort); + logger.info("listening on port: " + strPort + " for DogStatsD metrics"); + } + } + } if (httpJsonPorts != null) { for (String strPort : httpJsonPorts.split(",")) { if (strPort.trim().length() > 0) { @@ -95,7 +122,42 @@ protected void startOpenTsdbListener(String strPort) { agentAPI, agentId, port, prefix, pushLogLevel, pushValidationLevel, pushFlushInterval, pushBlockedSamples, null, opentsdbWhitelistRegex, opentsdbBlacklistRegex); - new Thread(new Ingester(graphiteHandler, port)).start(); + new Thread(new StringLineIngester(graphiteHandler, port)).start(); + } + + protected void startDogStatsDListener(String strPort) { + int port = Integer.parseInt(strPort); + + // Set up a custom graphite handler, with no formatter + ChannelHandler handler = new DogStatsDUDPHandler(agentAPI, agentId, port, prefix, pushLogLevel, pushValidationLevel, pushFlushInterval, pushBlockedSamples); + new Thread(new UdpIngester(handler, port)).start(); + } + + protected void startDataDogAgentListener(String strPort) { + int port = Integer.parseInt(strPort); + // decoders + List> decoders = new ArrayList<>(); + decoders.add(new Function() { + @Override + public ChannelHandler apply(Channel input) { + return new HttpRequestDecoder(); + } + }); + decoders.add(new Function() { + @Override + public ChannelHandler apply(Channel input) { + return new HttpResponseEncoder(); + } + }); + decoders.add(new Function() { + @Override + public ChannelHandler apply(Channel input) { + return new HttpObjectAggregator(1048576); + } + }); + + ChannelHandler handler = new DataDogAgentHandler(agentAPI, agentId, port, prefix, pushLogLevel, pushValidationLevel, pushFlushInterval, pushBlockedSamples); + new Thread(new TcpIngester(decoders, handler, port)).start(); } protected void startGraphiteListener(String strPort, @@ -108,16 +170,17 @@ protected void startGraphiteListener(String strPort, pushBlockedSamples, formatter, whitelistRegex, blacklistRegex); if (formatter == null) { - List> handler = Lists.newArrayList(1); - handler.add(new Function() { + List> handler = Lists.newArrayList(1); + handler.add(new Function() { @Override - public ChannelHandler apply(SocketChannel input) { - return new GraphiteHostAnnotator(input.remoteAddress().getHostName(), customSourceTags); + public ChannelHandler apply(Channel input) { + SocketChannel ch = (SocketChannel)input; + return new GraphiteHostAnnotator(ch.remoteAddress().getHostName(), customSourceTags); } }); - new Thread(new Ingester(handler, graphiteHandler, port)).start(); + new Thread(new StringLineIngester(handler, graphiteHandler, port)).start(); } else { - new Thread(new Ingester(graphiteHandler, port)).start(); + new Thread(new StringLineIngester(graphiteHandler, port)).start(); } } From 30a55af6a9d08e19219e8f4ed7ccaf40c4f010e9 Mon Sep 17 00:00:00 2001 From: Mike McLaughlin Date: Wed, 17 Feb 2016 15:05:40 -0800 Subject: [PATCH 05/10] look for datadog ports in the configuration file --- proxy/src/main/java/com/wavefront/agent/AbstractAgent.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/proxy/src/main/java/com/wavefront/agent/AbstractAgent.java b/proxy/src/main/java/com/wavefront/agent/AbstractAgent.java index 96a5f60f0..d22ea3f5f 100644 --- a/proxy/src/main/java/com/wavefront/agent/AbstractAgent.java +++ b/proxy/src/main/java/com/wavefront/agent/AbstractAgent.java @@ -270,6 +270,8 @@ private void loadListenerConfigurationFile() throws IOException { opentsdbBlacklistRegex = prop.getProperty("opentsdbBlacklistRegex", opentsdbBlacklistRegex); splitPushWhenRateLimited = Boolean.parseBoolean(prop.getProperty("splitPushWhenRateLimited", String.valueOf(splitPushWhenRateLimited))); + datadogAgentPorts = prop.getProperty("datadogPorts", datadogAgentPorts); + dogstatsdPorts = prop.getProperty("dogstatsdPorts", dogstatsdPorts); retryBackoffBaseSeconds = Double.parseDouble(prop.getProperty("retryBackoffBaseSeconds", String.valueOf(retryBackoffBaseSeconds))); customSourceTagsProperty = prop.getProperty("customSourceTags", customSourceTagsProperty); From e3ec1781e384f4660420039bf9ff327ad013b34e Mon Sep 17 00:00:00 2001 From: Mike McLaughlin Date: Wed, 17 Feb 2016 15:06:27 -0800 Subject: [PATCH 06/10] remove filter by type for datadog metrics --- .../com/wavefront/agent/DataDogAgentHandler.java | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/proxy/src/main/java/com/wavefront/agent/DataDogAgentHandler.java b/proxy/src/main/java/com/wavefront/agent/DataDogAgentHandler.java index 1a0c26cec..263df370f 100644 --- a/proxy/src/main/java/com/wavefront/agent/DataDogAgentHandler.java +++ b/proxy/src/main/java/com/wavefront/agent/DataDogAgentHandler.java @@ -314,24 +314,8 @@ private void handleApiSeries(final JsonNode root) { // ignore everything else and get the "series" array final JsonNode metrics = root.findPath("series"); for (final JsonNode metric : metrics) { - // we currently only support: gauge, histogram - final JsonNode type = metric.findPath("type"); - if (!type.asText().equalsIgnoreCase("gauge") && - !type.asText().equalsIgnoreCase("rate")) { - LOG.warning(String.format("Ignoring '%s' metric type (%s)", type.asText(), root.toString())); - continue; - } String metricName = metric.findPath("metric").asText(); - // rate types are created from histograms and seem to end - // in "count" which is a little confusing so add ".rate" - // to the end to clarify. - // NOTE: will need to check that all "rate" types are from the - // histogram - if (type.asText().equalsIgnoreCase("rate")) { - metricName += ".rate"; - } - // grab the tags final Map annotations = new HashMap<>(); final JsonNode tags = metric.findPath("tags"); From 8a7809ced1eb744ac68ab8d999c709f542654701 Mon Sep 17 00:00:00 2001 From: Mike McLaughlin Date: Thu, 18 Feb 2016 09:25:00 -0800 Subject: [PATCH 07/10] added documentation for datadog agent integration --- docs/DataDogAdapter.md | 66 ++++++++++++++++++++++++++++++++ docs/direct_to_wf.png | Bin 0 -> 35454 bytes docs/with_duplicating_proxy.png | Bin 0 -> 54156 bytes 3 files changed, 66 insertions(+) create mode 100644 docs/DataDogAdapter.md create mode 100644 docs/direct_to_wf.png create mode 100644 docs/with_duplicating_proxy.png diff --git a/docs/DataDogAdapter.md b/docs/DataDogAdapter.md new file mode 100644 index 000000000..41c5fc2ba --- /dev/null +++ b/docs/DataDogAdapter.md @@ -0,0 +1,66 @@ +# Overview +The Wavefront proxy accepts data in a number of formats. One of those formats is the JSON format created by the DataDog agent as well as the DogStatsD format. This document outlines the how the Wavefront proxy can be configured to accept metrics coming from the DataDog agent or DogStatsD programs. + +# Configuration Options +## Send metrics to Wavefront only + +![Metrics sent directly to Wavefront](direct_to_wf.png) + +## Send metrics to both DataDog and Wavefront + +![Duplicate traffic to both DataDog and Wavefront](with_duplicating_proxy.png) + +# Install and Run Wavefront Proxy +1. Install the Wavefront proxy with this command line: + +```$ sudo bash -c "$(curl -sL https://goo.gl/c70QCx)"``` +Follow the prompts to install the proxy on the box you are setting up. Your Wavefront URL is in the form of: https://{instance name}.wavefront.com/api/. The API token can be obtained from the bottom of this page: https://{instance name}.wavefront.com/settings/profile. + +2. (Optional) If you are running the Wavefront proxy on the same machine as you are running the DataDog agent, then you’ll need to add/update 2 configuration options. +``` +$ sudo vi /opt/wavefront/wavefront-proxy/conf/wavefront.conf +``` +Add (to the bottom of the file): +``` +dogstatsdPorts=9125 +datadogPorts=9126 +``` + +3. (Re)start the Wavefront Proxy +``` +$ sudo service wavefront-proxy restart +``` + +# (Optional) Install a HTTP duplicator (to send to both DataDog and to Wavefront). +*If you would like to send your data to both DataDog and Wavefront during your PoC, please complete the steps in this section. If you are not sending your metrics to both servers, then you can skip this section.* + +1. We’ve tested internally with teeproxy (an open source Go script), but there are other solutions out there. teeproxy is available from https://github.com/chrislusf/teeproxy. + +``` +$ git clone https://github.com/chrislusf/teeproxy.git +$ cd teeproxy; go build +``` +The teeproxy has this usage: +``` +./teeproxy -debug=true -l : -a -b +``` +An example: +``` +$ ./teeproxy -l : -a -b :8126 +(Example: ./teeproxy -l :8090 -a localhost:8087 -b localhost:8126) +``` + +The results from server “b” are ignored and the results server “a” will be returned to the DD agent. The Wavefront proxy is listening on port 8126 for DataDog agent requests. +***NOTE:** If you added the 2 configuration items in step #3 in previous section, then you’ll need to change this to port 9126 (or whatever port you entered in the configuration file).* + + +# Update DataDog Agent Configuration + +1. Update the **dd_url** in the **datadog.conf** configuration file to point to one of: +* the teeproxy listener (in the example above it’s http://localhost:{PORT TO LISTEN ON}) +* the Wavefront proxy (the default is http://localhost:8126) + +2. Restart the DataDog supervisor/agent process. +3. Test to make sure everything is working. + + diff --git a/docs/direct_to_wf.png b/docs/direct_to_wf.png new file mode 100644 index 0000000000000000000000000000000000000000..07a568c095c46d45318b66a9427e212d974c7ee1 GIT binary patch literal 35454 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 LZN)RyadvJGmcXxMpx3~Fy-|c&Q^zFXy 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 b*!ag@r#W-3#Ez#?3_#%N>gTe~DWM4fX7!xV literal 0 HcmV?d00001 From b89b821540e30dbd548c822aa9a686526b803d78 Mon Sep 17 00:00:00 2001 From: Mike McLaughlin Date: Tue, 1 Mar 2016 10:16:23 -0800 Subject: [PATCH 08/10] pom.xml: add SLF4J dependencies to remove error 'SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder"' --- proxy/pom.xml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/proxy/pom.xml b/proxy/pom.xml index 77597b70f..c4523ebe2 100644 --- a/proxy/pom.xml +++ b/proxy/pom.xml @@ -102,6 +102,21 @@ com.google.code.findbugs jsr305 + + org.slf4j + slf4j-api + 1.7.5 + + + org.slf4j + slf4j-simple + 1.6.4 + + + org.slf4j + slf4j-log4j12 + 1.7.5 + @@ -190,4 +205,4 @@ - \ No newline at end of file + From 237e4281410d2e89f6d79b6f00df50b059360c81 Mon Sep 17 00:00:00 2001 From: Mike McLaughlin Date: Tue, 1 Mar 2016 10:18:37 -0800 Subject: [PATCH 09/10] added debug logging to help track down missing data with opentsdb decoder --- .../java/com/wavefront/ingester/OpenTSDBDecoder.java | 10 ++++++++++ .../java/com/wavefront/agent/ChannelStringHandler.java | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/java-lib/src/main/java/com/wavefront/ingester/OpenTSDBDecoder.java b/java-lib/src/main/java/com/wavefront/ingester/OpenTSDBDecoder.java index eab2f734e..ad8aff5c6 100644 --- a/java-lib/src/main/java/com/wavefront/ingester/OpenTSDBDecoder.java +++ b/java-lib/src/main/java/com/wavefront/ingester/OpenTSDBDecoder.java @@ -3,6 +3,8 @@ import com.google.common.base.Preconditions; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import sunnylabs.report.ReportPoint; @@ -14,6 +16,7 @@ * @author Clement Pang (clement@wavefront.com). */ public class OpenTSDBDecoder implements Decoder { + protected static final Logger logger = Logger.getLogger("OpenTSDBDecoder"); private final String hostName; private static final IngesterFormatter FORMAT = IngesterFormatter.newBuilder().whiteSpace() @@ -39,6 +42,7 @@ public OpenTSDBDecoder(String hostName, List customSourceTags) { @Override public void decodeReportPoints(String msg, List out, String customerId) { + logger.fine("Decoding OpenTSDB point " + msg); ReportPoint point = FORMAT.drive(msg, hostName, customerId, customSourceTags); if (out != null) { out.add(point); @@ -47,9 +51,15 @@ public void decodeReportPoints(String msg, List out, String custome @Override public void decodeReportPoints(String msg, List out) { + logger.fine("Decoding OpenTSDB point " + msg); ReportPoint point = FORMAT.drive(msg, hostName, "dummy", customSourceTags); if (out != null) { out.add(point); } } + + @Override + public String toString() { + return "Open TSDB Decoder"; + } } diff --git a/proxy/src/main/java/com/wavefront/agent/ChannelStringHandler.java b/proxy/src/main/java/com/wavefront/agent/ChannelStringHandler.java index 0bbd44971..8bc67d1a9 100644 --- a/proxy/src/main/java/com/wavefront/agent/ChannelStringHandler.java +++ b/proxy/src/main/java/com/wavefront/agent/ChannelStringHandler.java @@ -103,6 +103,7 @@ protected boolean passesWhiteAndBlackLists(String pointLine) { @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { + logger.finer("Processing message " + msg); // ignore empty lines. if (msg == null || msg.trim().length() == 0) return; if (transformer != null) { @@ -143,4 +144,13 @@ private void handleBlockedPoint(String pointLine) { public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { // ignore. } + + @Override + public String toString() { + if (decoder == null) { + return "Channel String Handler (no decoder)"; + } else { + return "Channel String Handler (" + decoder.toString() + ")"; + } + } } From 21d486d36a60e79508393edc22261752ce7b560a Mon Sep 17 00:00:00 2001 From: Mike McLaughlin Date: Tue, 1 Mar 2016 10:18:56 -0800 Subject: [PATCH 10/10] fixed bug that dropped messages for non-graphite ingesters --- .../com/wavefront/ingester/StringLineIngester.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/java-lib/src/main/java/com/wavefront/ingester/StringLineIngester.java b/java-lib/src/main/java/com/wavefront/ingester/StringLineIngester.java index 0f1310f72..b93c9e846 100644 --- a/java-lib/src/main/java/com/wavefront/ingester/StringLineIngester.java +++ b/java-lib/src/main/java/com/wavefront/ingester/StringLineIngester.java @@ -24,7 +24,7 @@ public StringLineIngester(List> decoders, } public StringLineIngester(ChannelHandler commandHandler, int port) { - super(commandHandler, port); + super(createDecoderList(null), commandHandler, port); } /** @@ -34,8 +34,12 @@ public StringLineIngester(ChannelHandler commandHandler, int port) { * @return copy of the provided list with additional decodiers prepended */ private static List> createDecoderList(final List> decoders) { - final List> copy = - new ArrayList<>(decoders); + final List> copy; + if (decoders == null) { + copy = new ArrayList<>(); + } else { + copy = new ArrayList<>(decoders); + } copy.add(0, new Function() { @Override public ChannelHandler apply(Channel input) {