diff --git a/internal-api/src/jmh/java/datadog/trace/api/datastreams/DataStreamsTagsBenchmark.java b/internal-api/src/jmh/java/datadog/trace/api/datastreams/DataStreamsTagsBenchmark.java
new file mode 100644
index 00000000000..f7435a5f9c4
--- /dev/null
+++ b/internal-api/src/jmh/java/datadog/trace/api/datastreams/DataStreamsTagsBenchmark.java
@@ -0,0 +1,66 @@
+package datadog.trace.api.datastreams;
+
+import static datadog.trace.api.datastreams.DataStreamsTags.Direction.INBOUND;
+
+import java.util.concurrent.TimeUnit;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+
+/**
+ * Building the {@link DataStreamsTags} a Kafka consumer builds per record. With Data Streams off,
+ * nothing reads the tags ({@link #dsmOff}); with it on, the checkpoint reads their hashes ({@link
+ * #dsmOn}). Run with {@code -prof gc} for allocation per operation. The service name override is
+ * installed, as Data Streams monitoring does at startup, so building the tags reads it.
+ *
+ *
Results (MacBook, JDK 21, default flags, 5 forks via {@code -Pjmh.forks=5}, 4 threads; the
+ * machine was busy, so treat the times as approximate):
+ *
+ *
+ * Benchmark eager tags (before) lazy tags (after)
+ * ns/op B/op ns/op B/op
+ * dsmOff 230.4 +- 4.4 1464 8.5 +- 0.1 112
+ * dsmOn 226.9 +- 3.4 1464 118.4 +- 1.6 112
+ *
+ */
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Fork(2)
+@Warmup(iterations = 5, time = 1)
+@Measurement(iterations = 5, time = 1)
+@Threads(4)
+@State(Scope.Benchmark)
+public class DataStreamsTagsBenchmark {
+ // Per-consumer values, the same instances on every record as in the instrumentation
+ final String topic = "orders";
+ final String group = "order-processor";
+ final String clusterId = "lkc-6ojk2q";
+
+ @Setup(Level.Trial)
+ public void installServiceNameOverride() {
+ DataStreamsTags.setServiceNameOverride(new ThreadLocal<>());
+ }
+
+ @Benchmark
+ public DataStreamsTags dsmOff() {
+ return DataStreamsTags.create("kafka", INBOUND, topic, group, clusterId);
+ }
+
+ @Benchmark
+ public DataStreamsTags dsmOn() {
+ DataStreamsTags tags = DataStreamsTags.create("kafka", INBOUND, topic, group, clusterId);
+ // Returned, not just its hashes, since the tags escape into the checkpoint
+ tags.getHash();
+ tags.getAggregationHash();
+ return tags;
+ }
+}
diff --git a/internal-api/src/main/java/datadog/trace/api/datastreams/DataStreamsTags.java b/internal-api/src/main/java/datadog/trace/api/datastreams/DataStreamsTags.java
index df3eb214813..43435d459a9 100644
--- a/internal-api/src/main/java/datadog/trace/api/datastreams/DataStreamsTags.java
+++ b/internal-api/src/main/java/datadog/trace/api/datastreams/DataStreamsTags.java
@@ -13,29 +13,35 @@ public enum Direction {
public static DataStreamsTags EMPTY = DataStreamsTags.create(null, null);
+ // Hashes are computed on first read, since with Data Streams off nothing reads them
private long hash;
private long aggregationHash;
private long completeHash;
private int nonNullSize;
+ private volatile boolean hashed;
+ // Read when the tags are built, as they depend on the time and thread of the checkpoint
+ private final long baseHash;
+ private final String serviceNameOverrideValue;
+
+ // Tag values; the "name:value" tags are built from them only when read
// hash tags
- protected final String bus;
- protected final String direction;
- protected final Direction directionValue;
- protected final String exchange;
- protected final String topic;
- protected final String type;
- protected final String subscription;
+ private final String bus;
+ private final Direction directionValue;
+ private final String exchange;
+ private final String topic;
+ private final String type;
+ private final String subscription;
// additional grouping tags
- protected final String datasetName;
- protected final String datasetNamespace;
- protected final String isManual;
+ private final String datasetName;
+ private final String datasetNamespace;
+ private final Boolean isManual;
// informational tags
- protected final String group;
- protected final String consumerGroup;
- protected final String hasRoutingKey;
- protected final String kafkaClusterId;
- protected final String partition;
+ private final String group;
+ private final String consumerGroup;
+ private final Boolean hasRoutingKey;
+ private final String kafkaClusterId;
+ private final String partition;
public static final String MANUAL_TAG = "manual_checkpoint";
public static final String TYPE_TAG = "type";
@@ -145,92 +151,28 @@ public static DataStreamsTags createWithPartition(
/// For usage in tests *only*
public Boolean hasAllTags(String[] tags) {
for (String tag : tags) {
- if (tag.indexOf(':') == -1) {
+ int separator = tag.indexOf(':');
+ if (separator == -1) {
return false;
}
- String key = tag.substring(0, tag.indexOf(':'));
- String value = tag.substring(tag.indexOf(':') + 1);
- switch (key) {
- case BUS_TAG:
- if (!Objects.equals(this.bus, tag)) {
- return false;
- }
- break;
- case DIRECTION_TAG:
- if (!Objects.equals(
- this.directionValue,
- Objects.equals(value, "out") ? Direction.OUTBOUND : Direction.INBOUND)) {
- return false;
- }
- break;
- case EXCHANGE_TAG:
- if (!Objects.equals(this.exchange, tag)) {
- return false;
- }
- break;
- case TOPIC_TAG:
- if (!Objects.equals(this.topic, tag)) {
- return false;
- }
- break;
- case TYPE_TAG:
- if (!Objects.equals(this.type, tag)) {
- return false;
- }
- break;
- case SUBSCRIPTION_TAG:
- if (!Objects.equals(this.subscription, tag)) {
- return false;
- }
- break;
- case DATASET_NAME_TAG:
- if (!Objects.equals(this.datasetName, tag)) {
- return false;
- }
- break;
- case DATASET_NAMESPACE_TAG:
- if (!Objects.equals(this.datasetNamespace, tag)) {
- return false;
- }
- break;
- case MANUAL_TAG:
- if (!Objects.equals(this.isManual, tag)) {
- return false;
- }
- break;
- case GROUP_TAG:
- if (!Objects.equals(this.group, tag)) {
- return false;
- }
- break;
- case CONSUMER_GROUP_TAG:
- if (!Objects.equals(this.consumerGroup, tag)) {
- return false;
- }
- break;
- case HAS_ROUTING_KEY_TAG:
- if (!Objects.equals(this.hasRoutingKey, tag)) {
- return false;
- }
- break;
- case KAFKA_CLUSTER_ID_TAG:
- if (!Objects.equals(this.kafkaClusterId, tag)) {
- return false;
- }
- break;
- case PARTITION_TAG:
- if (!Objects.equals(this.partition, tag)) {
- return false;
- }
- break;
- default:
- return false;
+ int index = indexOfName(tag.substring(0, separator));
+ if (index == -1 || !Objects.equals(tagByIndex(index), tag)) {
+ return false;
}
}
return true;
}
+ private int indexOfName(String name) {
+ for (int i = 0; i < size(); i++) {
+ if (nameByIndex(i).equals(name)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
public static DataStreamsTags createWithGroup(
String type, Direction direction, String topic, String group) {
return new DataStreamsTags(
@@ -301,68 +243,78 @@ public DataStreamsTags(
Boolean hasRoutingKey,
String kafkaClusterId,
String partition) {
- this.bus = bus != null ? BUS_TAG + ":" + bus : null;
+ this.bus = bus;
this.directionValue = direction;
- if (direction == Direction.INBOUND) {
- this.direction = DIRECTION_TAG + ":in";
- } else if (direction == Direction.OUTBOUND) {
- this.direction = DIRECTION_TAG + ":out";
- } else {
- this.direction = null;
+ this.exchange = exchange;
+ this.topic = topic;
+ this.type = type;
+ this.subscription = subscription;
+ this.datasetName = datasetName;
+ this.datasetNamespace = datasetNamespace;
+ this.isManual = isManual;
+ this.group = group;
+ this.consumerGroup = consumerGroup;
+ this.hasRoutingKey = hasRoutingKey;
+ this.kafkaClusterId = kafkaClusterId;
+ this.partition = partition;
+
+ this.baseHash = BaseHash.getBaseHash();
+ ThreadLocal serviceNameOverride = DataStreamsTags.serviceNameOverride;
+ this.serviceNameOverrideValue = serviceNameOverride != null ? serviceNameOverride.get() : null;
+ }
+
+ private void ensureHashed() {
+ if (hashed) {
+ return;
}
- this.exchange = exchange != null ? EXCHANGE_TAG + ":" + exchange : null;
- this.topic = topic != null ? TOPIC_TAG + ":" + topic : null;
- this.type = type != null ? TYPE_TAG + ":" + type : null;
- this.subscription = subscription != null ? SUBSCRIPTION_TAG + ":" + subscription : null;
- this.datasetName = datasetName != null ? DATASET_NAME_TAG + ":" + datasetName : null;
- this.datasetNamespace =
- datasetNamespace != null ? DATASET_NAMESPACE_TAG + ":" + datasetNamespace : null;
- this.isManual = isManual != null ? MANUAL_TAG + ":" + isManual : null;
- this.group = group != null ? GROUP_TAG + ":" + group : null;
- this.consumerGroup = consumerGroup != null ? CONSUMER_GROUP_TAG + ":" + consumerGroup : null;
- this.hasRoutingKey = hasRoutingKey != null ? HAS_ROUTING_KEY_TAG + ":" + hasRoutingKey : null;
- this.kafkaClusterId =
- kafkaClusterId != null ? KAFKA_CLUSTER_ID_TAG + ":" + kafkaClusterId : null;
- this.partition = partition != null ? PARTITION_TAG + ":" + partition : null;
-
- this.hash = BaseHash.getBaseHash();
-
- if (DataStreamsTags.serviceNameOverride != null) {
- String val = DataStreamsTags.serviceNameOverride.get();
- if (val != null) {
- this.hash = FNV64Hash.continueHash(this.hash, val, FNV64Hash.Version.v1);
- }
+ // Racing threads compute the same values, so the race is benign
+ long hash = baseHash;
+ if (serviceNameOverrideValue != null) {
+ hash = FNV64Hash.continueHash(hash, serviceNameOverrideValue, FNV64Hash.Version.v1);
}
+ int nonNullSize = 0;
// hashable tags are 0-6: bus, direction, exchange, topic, type, subscription, kafkaClusterId
for (int i = 0; i < 7; i++) {
- String tag = this.tagByIndex(i);
- if (tag != null) {
- this.nonNullSize++;
- this.hash = FNV64Hash.continueHash(this.hash, tag, FNV64Hash.Version.v1);
+ String value = valueByIndex(i);
+ if (value != null) {
+ nonNullSize++;
+ hash = continueHash(hash, i, value);
}
}
// aggregation tags are 7-11: datasetName, datasetNamespace, isManual, group, consumerGroup
- this.aggregationHash = this.hash;
+ long aggregationHash = hash;
for (int i = 7; i < 12; i++) {
- String tag = this.tagByIndex(i);
- if (tag != null) {
- this.nonNullSize++;
- this.aggregationHash =
- FNV64Hash.continueHash(this.aggregationHash, tag, FNV64Hash.Version.v1);
+ String value = valueByIndex(i);
+ if (value != null) {
+ nonNullSize++;
+ aggregationHash = continueHash(aggregationHash, i, value);
}
}
// values are 12-13: partition, hasRoutingKey
- this.completeHash = aggregationHash;
- for (int i = 12; i < this.size(); i++) {
- String tag = this.tagByIndex(i);
- if (tag != null) {
- this.nonNullSize++;
- this.completeHash = FNV64Hash.continueHash(this.completeHash, tag, FNV64Hash.Version.v1);
+ long completeHash = aggregationHash;
+ for (int i = 12; i < size(); i++) {
+ String value = valueByIndex(i);
+ if (value != null) {
+ nonNullSize++;
+ completeHash = continueHash(completeHash, i, value);
}
}
+
+ this.hash = hash;
+ this.aggregationHash = aggregationHash;
+ this.completeHash = completeHash;
+ this.nonNullSize = nonNullSize;
+ this.hashed = true;
+ }
+
+ /** Continues {@code hash} over the tag {@code "name:value"}, without building it. */
+ private static long continueHash(long hash, int index, String value) {
+ hash = FNV64Hash.continueHash(hash, nameByIndex(index), FNV64Hash.Version.v1);
+ hash = FNV64Hash.continueHash(hash, ":", FNV64Hash.Version.v1);
+ return FNV64Hash.continueHash(hash, value, FNV64Hash.Version.v1);
}
public int size() {
@@ -372,12 +324,22 @@ public int size() {
// WARNING: DO NOT REORDER! Indices 0-6 are hash tags, 7-11 are aggregation tags, 12-13 are
// values. Reordering breaks hashing!
+ /**
+ * The {@code name:value} tag at {@code index}, or null if it has no value. Builds a new string on
+ * each call, as do the {@code get*} tag getters, so they're meant for serialization and tests,
+ * not per-message use.
+ */
public String tagByIndex(int index) {
+ String value = valueByIndex(index);
+ return value != null ? nameByIndex(index) + ":" + value : null;
+ }
+
+ private String valueByIndex(int index) {
switch (index) {
case 0:
return this.bus;
case 1:
- return this.direction;
+ return directionTagValue();
case 2:
return this.exchange;
case 3:
@@ -393,7 +355,7 @@ public String tagByIndex(int index) {
case 8:
return this.datasetNamespace;
case 9:
- return this.isManual;
+ return toString(this.isManual);
case 10:
return this.group;
case 11:
@@ -401,34 +363,82 @@ public String tagByIndex(int index) {
case 12:
return this.partition;
case 13:
- return this.hasRoutingKey;
+ return toString(this.hasRoutingKey);
default:
return null;
}
}
+ private static String nameByIndex(int index) {
+ switch (index) {
+ case 0:
+ return BUS_TAG;
+ case 1:
+ return DIRECTION_TAG;
+ case 2:
+ return EXCHANGE_TAG;
+ case 3:
+ return TOPIC_TAG;
+ case 4:
+ return TYPE_TAG;
+ case 5:
+ return SUBSCRIPTION_TAG;
+ case 6:
+ return KAFKA_CLUSTER_ID_TAG;
+ case 7:
+ return DATASET_NAME_TAG;
+ case 8:
+ return DATASET_NAMESPACE_TAG;
+ case 9:
+ return MANUAL_TAG;
+ case 10:
+ return GROUP_TAG;
+ case 11:
+ return CONSUMER_GROUP_TAG;
+ case 12:
+ return PARTITION_TAG;
+ case 13:
+ return HAS_ROUTING_KEY_TAG;
+ default:
+ return null;
+ }
+ }
+
+ private String directionTagValue() {
+ if (directionValue == Direction.INBOUND) {
+ return "in";
+ } else if (directionValue == Direction.OUTBOUND) {
+ return "out";
+ }
+ return null;
+ }
+
+ private static String toString(Boolean value) {
+ return value != null ? value.toString() : null;
+ }
+
public String getDirection() {
- return this.direction;
+ return tagByIndex(1);
}
public String getTopic() {
- return this.topic;
+ return tagByIndex(3);
}
public String getType() {
- return this.type;
+ return tagByIndex(4);
}
public String getIsManual() {
- return this.isManual;
+ return tagByIndex(9);
}
public String getBus() {
- return this.bus;
+ return tagByIndex(0);
}
public String getExchange() {
- return this.exchange;
+ return tagByIndex(2);
}
public Direction getDirectionValue() {
@@ -436,46 +446,49 @@ public Direction getDirectionValue() {
}
public String getSubscription() {
- return this.subscription;
+ return tagByIndex(5);
}
public String getDatasetName() {
- return this.datasetName;
+ return tagByIndex(7);
}
public String getDatasetNamespace() {
- return this.datasetNamespace;
+ return tagByIndex(8);
}
public String getGroup() {
- return this.group;
+ return tagByIndex(10);
}
public String getConsumerGroup() {
- return this.consumerGroup;
+ return tagByIndex(11);
}
public String getPartition() {
- return this.partition;
+ return tagByIndex(12);
}
public String getKafkaClusterId() {
- return this.kafkaClusterId;
+ return tagByIndex(6);
}
public String getHasRoutingKey() {
- return this.hasRoutingKey;
+ return tagByIndex(13);
}
public int nonNullSize() {
+ ensureHashed();
return this.nonNullSize;
}
public long getHash() {
+ ensureHashed();
return hash;
}
public long getAggregationHash() {
+ ensureHashed();
return aggregationHash;
}
@@ -485,11 +498,14 @@ public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
DataStreamsTags that = (DataStreamsTags) o;
+ ensureHashed();
+ that.ensureHashed();
return this.completeHash == that.completeHash;
}
@Override
public int hashCode() {
+ ensureHashed();
return Long.hashCode(this.completeHash);
}
@@ -497,37 +513,37 @@ public int hashCode() {
public String toString() {
return "DataStreamsTags{"
+ "bus='"
- + this.bus
+ + getBus()
+ "', direction='"
- + this.direction
+ + getDirection()
+ "', exchange='"
- + this.exchange
+ + getExchange()
+ "', topic='"
- + this.topic
+ + getTopic()
+ "', type='"
- + this.type
+ + getType()
+ "', subscription='"
- + this.subscription
+ + getSubscription()
+ "', datasetName='"
- + this.datasetName
+ + getDatasetName()
+ "', datasetNamespace='"
- + this.datasetNamespace
+ + getDatasetNamespace()
+ "', isManual="
- + this.isManual
+ + getIsManual()
+ "', group='"
- + this.group
+ + getGroup()
+ "', consumerGroup='"
- + this.consumerGroup
+ + getConsumerGroup()
+ "', hasRoutingKey='"
- + this.hasRoutingKey
+ + getHasRoutingKey()
+ "', kafkaClusterId='"
- + this.kafkaClusterId
+ + getKafkaClusterId()
+ "', partition='"
- + this.partition
+ + getPartition()
+ "', hash='"
- + hash
+ + getHash()
+ "', aggregationHash='"
- + aggregationHash
+ + getAggregationHash()
+ "', size='"
+ size();
}
diff --git a/internal-api/src/main/java/datadog/trace/util/FNV64Hash.java b/internal-api/src/main/java/datadog/trace/util/FNV64Hash.java
index ec0120b66db..f90d181a6a6 100644
--- a/internal-api/src/main/java/datadog/trace/util/FNV64Hash.java
+++ b/internal-api/src/main/java/datadog/trace/util/FNV64Hash.java
@@ -1,7 +1,5 @@
package datadog.trace.util;
-import java.nio.charset.StandardCharsets;
-
/**
* Calculates the FNV 64 bit hash. Longs should be treated as though they were unsigned
*
@@ -17,11 +15,41 @@ public enum Version {
}
public static long generateHash(String data, Version version) {
- return generateHash(data.getBytes(StandardCharsets.UTF_8), version);
+ return continueHash(FNV_INIT, data, version);
}
+ /**
+ * Continues the hash over the UTF-8 bytes of {@code data}, as {@link String#getBytes} would
+ * encode them (an unpaired surrogate becomes {@code '?'}), without allocating them.
+ */
public static long continueHash(long currentHash, String data, Version version) {
- return continueHash(currentHash, data.getBytes(StandardCharsets.UTF_8), version);
+ boolean v1 = version == Version.v1;
+ long hash = currentHash;
+ int length = data.length();
+ for (int i = 0; i < length; i++) {
+ char c = data.charAt(i);
+ if (c < 0x80) {
+ hash = hashByte(hash, c, v1);
+ } else if (c < 0x800) {
+ hash = hashByte(hash, 0xc0 | (c >> 6), v1);
+ hash = hashByte(hash, 0x80 | (c & 0x3f), v1);
+ } else if (!Character.isSurrogate(c)) {
+ hash = hashByte(hash, 0xe0 | (c >> 12), v1);
+ hash = hashByte(hash, 0x80 | ((c >> 6) & 0x3f), v1);
+ hash = hashByte(hash, 0x80 | (c & 0x3f), v1);
+ } else if (Character.isHighSurrogate(c)
+ && i + 1 < length
+ && Character.isLowSurrogate(data.charAt(i + 1))) {
+ int codePoint = Character.toCodePoint(c, data.charAt(++i));
+ hash = hashByte(hash, 0xf0 | (codePoint >> 18), v1);
+ hash = hashByte(hash, 0x80 | ((codePoint >> 12) & 0x3f), v1);
+ hash = hashByte(hash, 0x80 | ((codePoint >> 6) & 0x3f), v1);
+ hash = hashByte(hash, 0x80 | (codePoint & 0x3f), v1);
+ } else {
+ hash = hashByte(hash, '?', v1);
+ }
+ }
+ return hash;
}
public static long generateHash(byte[] data, Version version) {
@@ -45,6 +73,10 @@ public static long continueHash(
}
}
+ private static long hashByte(long hash, int b, boolean v1) {
+ return v1 ? (hash * FNV_PRIME) ^ b : (hash ^ b) * FNV_PRIME;
+ }
+
private static long generateFNV1Hash(long currentHash, byte[] data, int start, int length) {
long hash = currentHash;
diff --git a/internal-api/src/test/java/datadog/trace/api/datastreams/DataStreamsTagsTest.java b/internal-api/src/test/java/datadog/trace/api/datastreams/DataStreamsTagsTest.java
index 9bebbf5d7b6..89232d8659c 100644
--- a/internal-api/src/test/java/datadog/trace/api/datastreams/DataStreamsTagsTest.java
+++ b/internal-api/src/test/java/datadog/trace/api/datastreams/DataStreamsTagsTest.java
@@ -1,6 +1,13 @@
package datadog.trace.api.datastreams;
+import static datadog.trace.api.BaseHash.getBaseHash;
+import static datadog.trace.api.BaseHash.updateBaseHash;
+import static datadog.trace.api.datastreams.DataStreamsTags.Direction.INBOUND;
import static datadog.trace.api.datastreams.DataStreamsTags.Direction.OUTBOUND;
+import static datadog.trace.api.datastreams.DataStreamsTags.setServiceNameOverride;
+import static datadog.trace.util.FNV64Hash.Version.v1;
+import static datadog.trace.util.FNV64Hash.continueHash;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -9,12 +16,26 @@
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import datadog.trace.api.BaseHash;
import datadog.trace.api.datastreams.DataStreamsTags.Direction;
import java.nio.ByteBuffer;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class DataStreamsTagsTest {
+ private long originalBaseHash;
+
+ @BeforeEach
+ void captureBaseHash() {
+ originalBaseHash = getBaseHash();
+ }
+
+ /** The override and base hash are global, so a test changing them mustn't leak into others. */
+ @AfterEach
+ void restoreGlobals() {
+ setServiceNameOverride(null);
+ updateBaseHash(originalBaseHash);
+ }
private static DataStreamsTags getTags(int idx) {
return new DataStreamsTags(
@@ -98,6 +119,19 @@ void testHasAllTags() {
assertFalse(tags.hasAllTags(new String[] {"garbage"}));
}
+ @Test
+ void hasAllTagsRejectsTagsItDoesNotHave() {
+ DataStreamsTags tags = DataStreamsTags.create("kafka", INBOUND, "orders", "group", "cluster");
+
+ assertTrue(tags.hasAllTags(new String[] {"topic:orders", "direction:in", "group:group"}));
+ assertFalse(tags.hasAllTags(new String[] {"topic:payments"}), "different value");
+ assertFalse(tags.hasAllTags(new String[] {"exchange:orders"}), "absent tag");
+ assertFalse(tags.hasAllTags(new String[] {"unknown:orders"}), "unknown name");
+ assertFalse(tags.hasAllTags(new String[] {"direction:out"}), "different direction");
+ assertFalse(tags.hasAllTags(new String[] {"direction:garbage"}), "unknown direction");
+ assertFalse(tags.hasAllTags(new String[] {"topic:orders", "group:other"}), "one mismatch");
+ }
+
@Test
void testLongToBytes() {
long value = 123444L;
@@ -115,10 +149,10 @@ void testServiceNameOverrideAndGlobalHash() {
ThreadLocal serviceName = new ThreadLocal<>();
serviceName.set("test");
- DataStreamsTags.setServiceNameOverride(serviceName);
+ setServiceNameOverride(serviceName);
DataStreamsTags two = getTags(0);
- BaseHash.updateBaseHash(12);
+ updateBaseHash(12);
DataStreamsTags three = getTags(0);
assertNotEquals(one.getHash(), two.getHash());
@@ -489,4 +523,90 @@ void testAllThreeHashLevelsAreDifferentWhenAppropriateTagsChange() {
withPartition.getAggregationHash()); // aggregation hash unchanged
assertNotEquals(base, withPartition); // but complete hash changed
}
+
+ @Test
+ void hashesMatchHashingTheEncodedTags() {
+ DataStreamsTags[] tagsList = {
+ getTags(0),
+ DataStreamsTags.create(null, null),
+ DataStreamsTags.create("kafka", INBOUND, "orders", "group", "cluster"),
+ DataStreamsTags.createWithExchange("rabbitmq", OUTBOUND, "exchange", false),
+ DataStreamsTags.createWithDataset("s3", Direction.UNKNOWN, null, "caf\u00e9", "\u6ce8\ud83d"),
+ new DataStreamsTags(
+ "bus\ud83d\ude80",
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ "consumer",
+ null,
+ null,
+ "3")
+ };
+ for (DataStreamsTags tags : tagsList) {
+ assertHashesMatchEncodedTags(tags, getBaseHash(), null);
+ }
+ }
+
+ @Test
+ void hashesUseTheBaseHashAndServiceNameOverrideOfConstruction() {
+ long baseHash = getBaseHash();
+ ThreadLocal serviceName = new ThreadLocal<>();
+ serviceName.set("service-at-construction");
+ setServiceNameOverride(serviceName);
+ DataStreamsTags tags = getTags(1);
+
+ // Changed before the hashes are first read, which mustn't change them
+ serviceName.set("service-later");
+ updateBaseHash(baseHash + 1);
+
+ assertHashesMatchEncodedTags(tags, baseHash, "service-at-construction");
+ }
+
+ /** Checks the hashes against FNV-64 over the UTF-8 encoded "name:value" tags. */
+ private static void assertHashesMatchEncodedTags(
+ DataStreamsTags tags, long baseHash, String serviceName) {
+ long hash = baseHash;
+ if (serviceName != null) {
+ hash = hashEncoded(hash, serviceName);
+ }
+ int nonNullSize = 0;
+ for (int i = 0; i < 7; i++) {
+ String tag = tags.tagByIndex(i);
+ if (tag != null) {
+ nonNullSize++;
+ hash = hashEncoded(hash, tag);
+ }
+ }
+ long aggregationHash = hash;
+ for (int i = 7; i < 12; i++) {
+ String tag = tags.tagByIndex(i);
+ if (tag != null) {
+ nonNullSize++;
+ aggregationHash = hashEncoded(aggregationHash, tag);
+ }
+ }
+ long completeHash = aggregationHash;
+ for (int i = 12; i < tags.size(); i++) {
+ String tag = tags.tagByIndex(i);
+ if (tag != null) {
+ nonNullSize++;
+ completeHash = hashEncoded(completeHash, tag);
+ }
+ }
+ assertEquals(hash, tags.getHash(), tags::toString);
+ assertEquals(aggregationHash, tags.getAggregationHash(), tags::toString);
+ // The complete hash isn't exposed, but equals and hashCode are defined by it
+ assertEquals(Long.hashCode(completeHash), tags.hashCode(), tags::toString);
+ assertEquals(nonNullSize, tags.nonNullSize(), tags::toString);
+ }
+
+ private static long hashEncoded(long hash, String value) {
+ return continueHash(hash, value.getBytes(UTF_8), v1);
+ }
}
diff --git a/internal-api/src/test/java/datadog/trace/util/FNV64HashTest.java b/internal-api/src/test/java/datadog/trace/util/FNV64HashTest.java
index e534a87a944..301679f65ca 100644
--- a/internal-api/src/test/java/datadog/trace/util/FNV64HashTest.java
+++ b/internal-api/src/test/java/datadog/trace/util/FNV64HashTest.java
@@ -6,6 +6,7 @@
import static org.junit.jupiter.params.provider.Arguments.arguments;
import datadog.trace.util.FNV64Hash.Version;
+import java.nio.charset.StandardCharsets;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
@@ -234,6 +235,35 @@ private static Stream binaryTestCasesArguments() {
arguments(repeat(bytes(0x7f), 500), "e6be57375ad89b99", "39e9f18f2f85e221"));
}
+ @ParameterizedTest(name = "{index}: {0}")
+ @MethodSource("utf8Strings")
+ void stringHashMatchesItsUtf8Bytes(String scenario, String input) {
+ byte[] utf8 = input.getBytes(StandardCharsets.UTF_8);
+ for (Version version : Version.values()) {
+ assertEquals(generateHash(utf8, version), generateHash(input, version), scenario);
+ assertEquals(
+ continueHash(0x1234_5678_9abc_def0L, utf8, version),
+ continueHash(0x1234_5678_9abc_def0L, input, version),
+ scenario);
+ }
+ }
+
+ // A method source, not a table: the inputs are escapes for unpaired surrogates and other
+ // characters a table can't show readably
+ static Stream utf8Strings() {
+ return Stream.of(
+ arguments("ascii", "topic:orders"),
+ arguments("two-byte", "topic:caf\u00e9"),
+ arguments("three-byte", "topic:\u6ce8\u6587"),
+ arguments("max three-byte", "\uffff"),
+ arguments("supplementary", "topic:\ud83d\ude80"),
+ arguments("lone high surrogate", "a\ud83db"),
+ arguments("high surrogate at end", "a\ud83d"),
+ arguments("lone low surrogate", "a\ude80b"),
+ arguments("reversed pair", "\ude80\ud83d"),
+ arguments("high surrogate pair then lone", "\ud83d\ude80\ud83d"));
+ }
+
private static String repeat(String value, int times) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < times; i++) {