diff --git a/dogstatsd-http/core/src/main/java/com/datadoghq/dogstatsd/http/DirectHttpClient.java b/dogstatsd-http/core/src/main/java/com/datadoghq/dogstatsd/http/DirectHttpClient.java index 00266a8f..e05b6d76 100644 --- a/dogstatsd-http/core/src/main/java/com/datadoghq/dogstatsd/http/DirectHttpClient.java +++ b/dogstatsd-http/core/src/main/java/com/datadoghq/dogstatsd/http/DirectHttpClient.java @@ -22,6 +22,10 @@ /** * Simple Dogstatsd HTTP client for sending pre-aggregated metrics. * + *

By default, tags containing commas are split into separate tags and empty pieces are dropped, + * like the agent does for tags received over UDP or UDS. Splitting happens before the special tags + * below are handled. It can be disabled with {@link Builder#splitTags}. + * *

Two tags are given special treatment: their value is submitted as a property of the * timeseries. * @@ -45,6 +49,7 @@ public class DirectHttpClient { private final PayloadBuilder sketchesBuilder; private final Sketch sketchBuffer = new Sketch(); private final String prefix; + private final boolean splitTags; private static final int defaultInterval = 10; private static final String hostTagPrefix = "host:"; private static final String hostResourceType = "host"; @@ -69,6 +74,7 @@ private DirectHttpClient(final Builder builder) { } else { prefix = ""; } + splitTags = builder.splitTags; seriesBuilder = new PayloadBuilder( @@ -113,6 +119,7 @@ public static interface Forwarder { public static class Builder { private final Forwarder forwarder; private String prefix; + private boolean splitTags = true; private Builder(final Forwarder forwarder) { this.forwarder = Objects.requireNonNull(forwarder, "forwarder"); @@ -130,6 +137,18 @@ public Builder prefix(final String val) { return this; } + /** + * Sets whether tags containing commas are split into separate tags, as the agent does for + * tags received over UDP or UDS. Enabled by default. + * + * @param val true to split tags on commas, false to send every tag unchanged. + * @return this builder. + */ + public Builder splitTags(final boolean val) { + splitTags = val; + return this; + } + /** * Builds the client. * @@ -203,14 +222,50 @@ private String prefixed(final String name) { } /** - * Applies the tags to the metric, extracting the host tag into the host resource and the - * cardinality tag into the tags cardinality. The cardinality tag itself is kept in the tags. + * Applies the tags to the metric, splitting comma-separated tags if enabled, then extracting + * the host tag into the host resource and the cardinality tag into the tags cardinality. The + * cardinality tag itself is kept in the tags. */ - private static > T withTagsHostAndCardinality( + private > T withTagsHostAndCardinality( final T metric, final List tags) { - return metric.setTags(withoutHostTags(tags)) - .setResources(hostResource(hostTag(tags))) - .setTagsCardinality(cardinality(cardinalityTag(tags))); + final List t = splitTags ? splitOnComma(tags) : tags; + return metric.setTags(withoutHostTags(t)) + .setResources(hostResource(hostTag(t))) + .setTagsCardinality(cardinality(cardinalityTag(t))); + } + + /** + * Returns the tags with every tag containing a comma split into separate tags, dropping empty + * pieces, or the tags themselves if none contains a comma. + */ + static List splitOnComma(final List tags) { + if (tags == null) { + return null; + } + ArrayList split = null; + for (int i = 0; i < tags.size(); i++) { + final String tag = tags.get(i); + int end = tag.indexOf(','); + if (end >= 0) { + if (split == null) { + split = new ArrayList<>(tags.subList(0, i)); + } + int start = 0; + while (end >= 0) { + if (end > start) { + split.add(tag.substring(start, end)); + } + start = end + 1; + end = tag.indexOf(',', start); + } + if (start < tag.length()) { + split.add(tag.substring(start)); + } + } else if (split != null) { + split.add(tag); + } + } + return split == null ? tags : split; } /** Returns the value of the first host tag, or null if there is none. */ diff --git a/dogstatsd-http/core/src/test/java/com/datadoghq/dogstatsd/http/DirectHttpClientTest.java b/dogstatsd-http/core/src/test/java/com/datadoghq/dogstatsd/http/DirectHttpClientTest.java index 1cc699e7..38b02b35 100644 --- a/dogstatsd-http/core/src/test/java/com/datadoghq/dogstatsd/http/DirectHttpClientTest.java +++ b/dogstatsd-http/core/src/test/java/com/datadoghq/dogstatsd/http/DirectHttpClientTest.java @@ -203,6 +203,81 @@ public void withoutHostTagsRemovesEveryHostTag() { assertSame(card, DirectHttpClient.withoutHostTags(card)); } + @Test + public void splitOnCommaSplitsTags() { + assertNull(DirectHttpClient.splitOnComma(null)); + + List noComma = Arrays.asList("a:b", "", "c:d"); + assertSame(noComma, DirectHttpClient.splitOnComma(noComma)); + + assertEquals( + Arrays.asList("a:b", "c:d", "e:f"), + DirectHttpClient.splitOnComma(Arrays.asList("a:b,c:d", "e:f"))); + assertEquals( + Arrays.asList("x", "", "a", "b", "y", "z"), + DirectHttpClient.splitOnComma(Arrays.asList("x", "", "a,,b,", ",", ",y", "z"))); + } + + @Test + public void commaSeparatedTagsAreSplit() { + TestForwarder fwd = new TestForwarder(); + DirectHttpClient client = DirectHttpClient.builder(fwd).build(); + client.gauge("metric", 1.5, 100, Collections.singletonList("a:b,c:d")); + client.flush(); + + ArrayList expected = new ArrayList<>(); + PayloadBuilder b = builderInto(expected); + b.gauge("metric") + .setTags(Arrays.asList("a:b", "c:d")) + .setInterval(10) + .addPoint(100, 1.5) + .close(); + b.close(); + + assertSent(expected, fwd, seriesUri, "gauge with comma-separated tags"); + } + + @Test + public void hostAndCardinalityInsideCommaSeparatedTag() { + TestForwarder fwd = new TestForwarder(); + DirectHttpClient client = DirectHttpClient.builder(fwd).build(); + client.count( + "metric", 20, 100, Collections.singletonList("a:b,host:h1,dd.internal.card:low")); + client.flush(); + + ArrayList expected = new ArrayList<>(); + PayloadBuilder b = builderInto(expected); + b.rate("metric") + .setTags(Arrays.asList("a:b", "dd.internal.card:low")) + .setResources(Arrays.asList("host", "h1")) + .setTagsCardinality(TagsCardinality.LOW) + .setInterval(10) + .addPoint(100, 2) + .close(); + b.close(); + + assertSent(expected, fwd, seriesUri, "count with host and cardinality in a joined tag"); + } + + @Test + public void splitTagsCanBeDisabled() { + TestForwarder fwd = new TestForwarder(); + DirectHttpClient client = DirectHttpClient.builder(fwd).splitTags(false).build(); + client.gauge("metric", 1.5, 100, Collections.singletonList("a:b,host:h1")); + client.flush(); + + ArrayList expected = new ArrayList<>(); + PayloadBuilder b = builderInto(expected); + b.gauge("metric") + .setTags(Collections.singletonList("a:b,host:h1")) + .setInterval(10) + .addPoint(100, 1.5) + .close(); + b.close(); + + assertSent(expected, fwd, seriesUri, "gauge with splitting disabled"); + } + @Test public void hostResourceIsATypeNamePair() { assertNull(DirectHttpClient.hostResource(null));