Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.datadoghq.dogstatsd.http.serializer.Metric;
import com.datadoghq.dogstatsd.http.serializer.PayloadBuilder;
import com.datadoghq.dogstatsd.http.serializer.PayloadConsumer;
import com.datadoghq.dogstatsd.http.serializer.TagsCardinality;
import java.net.URI;
import java.nio.BufferOverflowException;
import java.util.ArrayList;
Expand All @@ -21,8 +22,17 @@
/**
* Simple Dogstatsd HTTP client for sending pre-aggregated metrics.
*
* <p>A {@code host:} tag is not sent as a tag: it is removed from the tags and submitted as the
* host resource of the timeseries.
* <p>Two tags are given special treatment: their value is submitted as a property of the
* timeseries.
*
* <ul>
* <li>{@code host:} — the value of the first one becomes the host resource. Every {@code host:}
* tag is removed from the tags.
* <li>{@code dd.internal.card:} — the value of the first one becomes the tags cardinality. Values
* the agent does not recognize ask for the cardinality the agent is configured to use. The
* tags are sent unchanged, so that agents that only understand the tag keep working; agents
* that understand the cardinality drop the tag themselves.
* </ul>
*
* <p>Not thread safe.
*
Expand All @@ -38,6 +48,7 @@ public class DirectHttpClient {
private static final int defaultInterval = 10;
private static final String hostTagPrefix = "host:";
private static final String hostResourceType = "host";
private static final String cardinalityTagPrefix = "dd.internal.card:";

/**
* Creates a builder for a client sending its payloads through the given forwarder.
Expand Down Expand Up @@ -135,13 +146,12 @@ public DirectHttpClient build() {
* @param name the metric name, to which the client prefix is prepended.
* @param value the gauge value.
* @param ts the timestamp of the point in seconds since Unix epoch.
* @param tags the tags to attach to the point. A {@code host:} tag is not attached as a tag:
* the first one is submitted as the host resource of the timeseries, and any further {@code
* host:} tags are dropped.
* @param tags the tags to attach to the point, see {@link DirectHttpClient} for the handling of
* special values.
* @throws BufferOverflowException if the encoded metric exceeds the maximum payload size.
*/
public void gauge(String name, double value, long ts, List<String> tags) {
withTagsAndHost(seriesBuilder.gauge(prefixed(name)), tags)
withTagsHostAndCardinality(seriesBuilder.gauge(prefixed(name)), tags)
.setInterval(defaultInterval)
.addPoint(ts, value)
.close();
Expand All @@ -156,13 +166,12 @@ public void gauge(String name, double value, long ts, List<String> tags) {
* @param name the metric name, to which the client prefix is prepended.
* @param value the count accumulated over the interval starting at {@code ts}.
* @param ts the timestamp of the point in seconds since Unix epoch.
* @param tags the tags to attach to the point. A {@code host:} tag is not attached as a tag:
* the first one is submitted as the host resource of the timeseries, and any further {@code
* host:} tags are dropped.
* @param tags the tags to attach to the point, see {@link DirectHttpClient} for the handling of
* special values.
* @throws BufferOverflowException if the encoded metric exceeds the maximum payload size.
*/
public void count(String name, double value, long ts, List<String> tags) {
withTagsAndHost(seriesBuilder.rate(prefixed(name)), tags)
withTagsHostAndCardinality(seriesBuilder.rate(prefixed(name)), tags)
.setInterval(defaultInterval)
.addPoint(ts, value / defaultInterval)
.close();
Expand All @@ -175,17 +184,16 @@ public void count(String name, double value, long ts, List<String> tags) {
* @param values the observations to summarize.
* @param sampleRate the sampling rate used to collect {@code values}, in {@code (0, 1]}.
* @param ts the timestamp of the point in seconds since Unix epoch.
* @param tags the tags to attach to the point. A {@code host:} tag is not attached as a tag:
* the first one is submitted as the host resource of the timeseries, and any further {@code
* host:} tags are dropped.
* @param tags the tags to attach to the point, see {@link DirectHttpClient} for the handling of
* special values.
* @throws IllegalArgumentException if {@code sampleRate} is {@code NaN}, not positive, or
* greater than 1.
* @throws BufferOverflowException if the encoded metric exceeds the maximum payload size.
*/
public void distribution(
String name, double[] values, double sampleRate, long ts, List<String> tags) {
sketchBuffer.build(values, sampleRate);
withTagsAndHost(sketchesBuilder.sketch(prefixed(name)), tags)
withTagsHostAndCardinality(sketchesBuilder.sketch(prefixed(name)), tags)
.addPoint(ts, sketchBuffer)
.close();
}
Expand All @@ -194,28 +202,64 @@ private String prefixed(final String name) {
return prefix.isEmpty() ? name : prefix + name;
}

/** Applies the tags to the metric, extracting the host tag into the host resource. */
private static <T extends Metric<T>> T withTagsAndHost(
/**
* 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.
*/
private static <T extends Metric<T>> T withTagsHostAndCardinality(
final T metric, final List<String> tags) {
final String host = hostTag(tags);
return metric.setTags(host == null ? tags : withoutHostTags(tags))
.setResources(hostResource(host));
return metric.setTags(withoutHostTags(tags))
.setResources(hostResource(hostTag(tags)))
.setTagsCardinality(cardinality(cardinalityTag(tags)));
}

/** Returns the value of the first host tag, or null if there is none. */
static String hostTag(final List<String> tags) {
return tagValue(tags, hostTagPrefix);
}

/** Returns the value of the first cardinality tag, or null if there is none. */
static String cardinalityTag(final List<String> tags) {
return tagValue(tags, cardinalityTagPrefix);
}

/** Returns the value of the first tag with the given prefix, or null if there is none. */
private static String tagValue(final List<String> tags, final String prefix) {
if (tags == null) {
return null;
}
for (int i = 0; i < tags.size(); i++) {
final String tag = tags.get(i);
if (tag.startsWith(hostTagPrefix)) {
return tag.substring(hostTagPrefix.length());
if (tag.startsWith(prefix)) {
return tag.substring(prefix.length());
}
}
return null;
}

/**
* Returns the cardinality constant matching the value of a cardinality tag. Values the agent
* does not recognize, including null, ask for the cardinality the agent is configured to use.
*/
static TagsCardinality cardinality(final String value) {
if (value == null) {
return TagsCardinality.DEFAULT;
}
if (value.equalsIgnoreCase("none")) {
return TagsCardinality.NONE;
}
if (value.equalsIgnoreCase("low")) {
return TagsCardinality.LOW;
}
if (value.equalsIgnoreCase("orchestrator") || value.equalsIgnoreCase("orch")) {
return TagsCardinality.ORCHESTRATOR;
}
if (value.equalsIgnoreCase("high")) {
return TagsCardinality.HIGH;
}
return TagsCardinality.DEFAULT;
}

/** Returns the tags with every host tag removed, or the tags themselves if there was none. */
static List<String> withoutHostTags(final List<String> tags) {
if (tags == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public abstract class Metric<T extends Metric<T>> {
List<String> tags = null;
List<String> resources = null;
int interval = 0;
TagsCardinality cardinality = TagsCardinality.DEFAULT;
Origin origin = Origin.dogstatsd;

Metric(PayloadBuilder pb, int type, String name) {
Expand Down Expand Up @@ -71,14 +72,27 @@ public T setInterval(int interval) {
return self();
}

/**
* Set the cardinality of the origin tags the agent attaches to this metric.
*
* <p>Ignored by the intake, which does not attach origin tags.
*
* @param cardinality The cardinality to request, or null for {@link TagsCardinality#DEFAULT}.
* @return This.
*/
public T setTagsCardinality(TagsCardinality cardinality) {
this.cardinality = cardinality == null ? TagsCardinality.DEFAULT : cardinality;
return self();
}

abstract T self();

abstract void encodeValues(ValueType valueType);

void encodeIndependentFields() {
ColumnarBuffer r = pb.currentRecord();
ValueType valueType = PointKind.of(pb.values).toValueType();
r.putUint64(Column.types, type | valueType.flag());
r.putUint64(Column.types, type | valueType.flag() | cardinality.flag());
r.putUint64(Column.intervals, interval);
r.putSint64(Column.sourceTypeNameRefs, 0);
encodeValues(valueType);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* Unless explicitly stated otherwise all files in this repository are
* licensed under the Apache 2.0 License.
*
* This product includes software developed at Datadog
* (https://www.datadoghq.com/) Copyright 2026 Datadog, Inc.
*/

package com.datadoghq.dogstatsd.http.serializer;

/**
* Cardinality of the origin tags the agent attaches to a metric.
*
* <p>See the <a
* href="https://docs.datadoghq.com/getting_started/tagging/assigning_tags/?tab=containerizedenvironments#tags-cardinality">tags
* cardinality documentation</a>.
*/
public enum TagsCardinality {
/** Requests the cardinality the agent is configured to use for dogstatsd metrics. */
DEFAULT(0x0000),

/** Requests no origin tags at all. */
NONE(0x1000),

/** Requests low cardinality origin tags. */
LOW(0x2000),

/** Requests orchestrator cardinality origin tags. */
ORCHESTRATOR(0x3000),

/** Requests high cardinality origin tags. */
HIGH(0x4000);

private final int flag;

TagsCardinality(int flag) {
this.flag = flag;
}

int flag() {
return flag;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import com.datadoghq.dogstatsd.Sketch;
import com.datadoghq.dogstatsd.http.serializer.PayloadBuilder;
import com.datadoghq.dogstatsd.http.serializer.PayloadConsumer;
import com.datadoghq.dogstatsd.http.serializer.TagsCardinality;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -196,6 +197,10 @@ public void withoutHostTagsRemovesEveryHostTag() {
Arrays.asList("a:b", "c:d"),
DirectHttpClient.withoutHostTags(
Arrays.asList("a:b", "host:h1", "c:d", "host:h2")));

// The cardinality tag is sent to the agent as a tag as well.
List<String> card = Arrays.asList("a:b", "dd.internal.card:low");
assertSame(card, DirectHttpClient.withoutHostTags(card));
}

@Test
Expand All @@ -204,4 +209,128 @@ public void hostResourceIsATypeNamePair() {
assertEquals(Arrays.asList("host", "h1"), DirectHttpClient.hostResource("h1"));
assertEquals(Arrays.asList("host", ""), DirectHttpClient.hostResource(""));
}

@Test
public void noCardinalityTagLeavesCardinalityDefault() {
TestForwarder fwd = new TestForwarder();
DirectHttpClient client = DirectHttpClient.builder(fwd).build();
client.gauge("metric", 1.5, 100, Collections.singletonList("a:b"));
client.flush();

ArrayList<byte[]> expected = new ArrayList<>();
PayloadBuilder b = builderInto(expected);
b.gauge("metric")
.setTags(Collections.singletonList("a:b"))
.setTagsCardinality(TagsCardinality.DEFAULT)
.setInterval(10)
.addPoint(100, 1.5)
.close();
b.close();

assertSent(expected, fwd, seriesUri, "gauge without cardinality");
}

@Test
public void cardinalityTagIsSentAsCardinality() {
TestForwarder fwd = new TestForwarder();
DirectHttpClient client = DirectHttpClient.builder(fwd).build();
client.gauge(
"gauge",
1.5,
100,
Arrays.asList("a:b", "dd.internal.card:high", "host:h1", "dd.internal.card:low"));
client.count("count", 20, 100, Arrays.asList("dd.internal.card:none", "a:b"));
client.flush();

ArrayList<byte[]> expected = new ArrayList<>();
PayloadBuilder b = builderInto(expected);
b.gauge("gauge")
.setTags(Arrays.asList("a:b", "dd.internal.card:high", "dd.internal.card:low"))
.setResources(Arrays.asList("host", "h1"))
.setTagsCardinality(TagsCardinality.HIGH)
.setInterval(10)
.addPoint(100, 1.5)
.close();
b.rate("count")
.setTags(Arrays.asList("dd.internal.card:none", "a:b"))
.setTagsCardinality(TagsCardinality.NONE)
.setInterval(10)
.addPoint(100, 2)
.close();
b.close();

assertSent(expected, fwd, seriesUri, "gauge and count with cardinality tag");
}

@Test
public void distributionSendsCardinalityTagAsCardinality() {
double[] values = new double[] {1, 2, 2};

TestForwarder fwd = new TestForwarder();
DirectHttpClient client = DirectHttpClient.builder(fwd).build();
client.distribution(
"metric", values, 1.0, 100, Arrays.asList("dd.internal.card:orch", "a:b"));
client.flush();

Sketch sketch = new Sketch();
sketch.build(values, 1.0);

ArrayList<byte[]> expected = new ArrayList<>();
PayloadBuilder b = builderInto(expected);
b.sketch("metric")
.setTags(Arrays.asList("dd.internal.card:orch", "a:b"))
.setTagsCardinality(TagsCardinality.ORCHESTRATOR)
.addPoint(100, sketch)
.close();
b.close();

assertSent(expected, fwd, sketchesUri, "distribution with cardinality tag");
}

@Test
public void unknownCardinalityTagLeavesCardinalityDefault() {
TestForwarder fwd = new TestForwarder();
DirectHttpClient client = DirectHttpClient.builder(fwd).build();
client.gauge("metric", 1.5, 100, Arrays.asList("a:b", "dd.internal.card:bogus"));
client.flush();

ArrayList<byte[]> expected = new ArrayList<>();
PayloadBuilder b = builderInto(expected);
b.gauge("metric")
.setTags(Arrays.asList("a:b", "dd.internal.card:bogus"))
.setInterval(10)
.addPoint(100, 1.5)
.close();
b.close();

assertSent(expected, fwd, seriesUri, "gauge with unknown cardinality tag");
}

@Test
public void cardinalityTagPicksFirstValue() {
assertNull(DirectHttpClient.cardinalityTag(null));
assertNull(DirectHttpClient.cardinalityTag(Collections.<String>emptyList()));
assertNull(DirectHttpClient.cardinalityTag(Arrays.asList("a:b", "dd.internal.cardx:low")));
assertEquals(
"low",
DirectHttpClient.cardinalityTag(
Arrays.asList("a:b", "dd.internal.card:low", "dd.internal.card:high")));
assertEquals(
"",
DirectHttpClient.cardinalityTag(Collections.singletonList("dd.internal.card:")));
}

@Test
public void cardinalityParsesTheValuesTheAgentAccepts() {
assertEquals(TagsCardinality.DEFAULT, DirectHttpClient.cardinality(null));
assertEquals(TagsCardinality.DEFAULT, DirectHttpClient.cardinality(""));
assertEquals(TagsCardinality.DEFAULT, DirectHttpClient.cardinality("bogus"));
assertEquals(TagsCardinality.NONE, DirectHttpClient.cardinality("none"));
assertEquals(TagsCardinality.LOW, DirectHttpClient.cardinality("low"));
assertEquals(TagsCardinality.ORCHESTRATOR, DirectHttpClient.cardinality("orch"));
assertEquals(TagsCardinality.ORCHESTRATOR, DirectHttpClient.cardinality("orchestrator"));
assertEquals(TagsCardinality.HIGH, DirectHttpClient.cardinality("high"));
// The agent parses the value case-insensitively.
assertEquals(TagsCardinality.HIGH, DirectHttpClient.cardinality("HIGH"));
}
}
Loading
Loading