diff --git a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/KnownTagsEmitter.kt b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/KnownTagsEmitter.kt
index a4c0c1e997e..8234a1be385 100644
--- a/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/KnownTagsEmitter.kt
+++ b/build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/KnownTagsEmitter.kt
@@ -209,6 +209,21 @@ object KnownTagsEmitter {
"""
};
+ /**
+ * Each tag's id, indexed by serial; slot 0 (no tag) is 0. Lets a store that keeps only the
+ * 16-bit serial recover the full id, flag and slot bits included.
+ */
+ static final long[] IDS_BY_SERIAL = {
+ 0L,
+ """.trimIndent()
+ )
+ for (t in reg.tags.sortedBy { it.serial }) {
+ appendLine(" ${idC(t.ddName)},")
+ }
+ appendLine(
+ """
+ };
+
/**
* The registry's name↔id tables, as a {@link KnownTagCodec.Resolver}. {@code KnownTagCodec}
* reads this field from its own holder, so the two classes complete each other: the codec owns
@@ -221,6 +236,11 @@ object KnownTagsEmitter {
int serial = KnownTagCodec.serialNum(tagId);
return serial < NAMES_BY_SERIAL.length ? NAMES_BY_SERIAL[serial] : null;
}
+
+ @Override
+ public long idOf(int serial) {
+ return IDS_BY_SERIAL[serial];
+ }
// openTelemetryNameOf: id -> OTel-namespace name on spans of a direction, null when
// the tag has none there. The caller (a serializer) owns any fall-back-to-Datadog-name
diff --git a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPluginTest.kt b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPluginTest.kt
index 5874dcfb70d..82d1b0de5ec 100644
--- a/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPluginTest.kt
+++ b/build-logic/tag-registry/src/test/kotlin/datadog/buildlogic/tagRegistry/TagRegistryGeneratorPluginTest.kt
@@ -247,6 +247,7 @@ class TagRegistryGeneratorPluginTest {
public final class KnownTagCodec {
public interface Resolver {
String nameOf(long id);
+ long idOf(int serial);
String openTelemetryNameOf(long id, int direction);
long keyOf(String name);
long directionalKeyOf(String name, int direction);
diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/BaseDecorator.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/BaseDecorator.java
index 55a8784aee3..cccac3c697e 100644
--- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/BaseDecorator.java
+++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/BaseDecorator.java
@@ -1,7 +1,5 @@
package datadog.trace.bootstrap.instrumentation.decorator;
-import static datadog.trace.bootstrap.instrumentation.java.net.HostNameResolver.hostName;
-
import datadog.appsec.api.blocking.BlockingException;
import datadog.context.Context;
import datadog.context.ContextScope;
@@ -15,8 +13,6 @@
import datadog.trace.bootstrap.instrumentation.api.ErrorPriorities;
import datadog.trace.bootstrap.instrumentation.api.SpanPrototype;
import java.lang.reflect.Method;
-import java.net.Inet4Address;
-import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.concurrent.ExecutionException;
@@ -194,11 +190,14 @@ protected void doOnError(final AgentSpan span, final Throwable throwable, byte e
throwable instanceof ExecutionException ? throwable.getCause() : throwable, errorPriority);
}
+ // The peer extractors write straight to the span rather than through setTagsFrom: resolving the
+ // hostname can mean a reverse DNS lookup, which mustn't run under the span's tag lock. To skip
+ // the lookup per span, freeze the extraction once per address instead (FrozenTags).
+
public final void onPeerConnection(
final AgentSpan span, @Nullable final InetSocketAddress remoteConnection) {
if (remoteConnection != null) {
- onPeerConnection(span, remoteConnection.getAddress(), !remoteConnection.isUnresolved());
- setPeerPort(span, remoteConnection.getPort());
+ PeerSocketAddress.INSTANCE.extract(remoteConnection, span);
}
}
@@ -210,15 +209,7 @@ public final void onPeerConnection(
public final void onPeerConnection(
AgentSpan span, @Nullable InetAddress remoteAddress, boolean resolved) {
if (remoteAddress != null) {
- String ip = remoteAddress.getHostAddress();
- if (resolved && Config.get().isPeerHostNameEnabled()) {
- span.setTag(KnownTags.PEER_HOSTNAME_ID, hostName(remoteAddress, ip));
- }
- if (remoteAddress instanceof Inet4Address) {
- span.setTag(KnownTags.PEER_IPV4_ID, ip);
- } else if (remoteAddress instanceof Inet6Address) {
- span.setTag(KnownTags.PEER_IPV6_ID, ip);
- }
+ (resolved ? PeerAddress.RESOLVED : PeerAddress.UNRESOLVED).extract(remoteAddress, span);
}
}
diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/PeerAddress.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/PeerAddress.java
new file mode 100644
index 00000000000..35691fade5d
--- /dev/null
+++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/PeerAddress.java
@@ -0,0 +1,47 @@
+package datadog.trace.bootstrap.instrumentation.decorator;
+
+import static datadog.trace.bootstrap.instrumentation.java.net.HostNameResolver.hostName;
+
+import datadog.trace.api.Config;
+import datadog.trace.api.KnownTags;
+import datadog.trace.bootstrap.instrumentation.api.SpanData;
+import datadog.trace.bootstrap.instrumentation.api.TagExtractor;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+
+/**
+ * Extracts the peer tags of a remote {@link InetAddress}: its IP, as {@code peer.ipv4} or {@code
+ * peer.ipv6}, and, when the address was resolved and peer hostnames are enabled, {@code
+ * peer.hostname}.
+ *
+ *
Resolving the hostname may do a reverse DNS lookup, so don't apply this with {@code
+ * span.setTagsFrom(address, extractor)}, which holds the span's tag lock while it runs: call {@code
+ * extract} on the span directly, or freeze it once per address ({@code FrozenTags.of}).
+ */
+public final class PeerAddress implements TagExtractor {
+ /** For an address that was resolved from a hostname. */
+ public static final PeerAddress RESOLVED = new PeerAddress(true);
+
+ /** For an address that wasn't resolved: no {@code peer.hostname}. */
+ public static final PeerAddress UNRESOLVED = new PeerAddress(false);
+
+ private final boolean resolved;
+
+ private PeerAddress(boolean resolved) {
+ this.resolved = resolved;
+ }
+
+ @Override
+ public void extract(InetAddress address, SpanData span) {
+ String ip = address.getHostAddress();
+ if (resolved && Config.get().isPeerHostNameEnabled()) {
+ span.setTag(KnownTags.PEER_HOSTNAME_ID, hostName(address, ip));
+ }
+ if (address instanceof Inet4Address) {
+ span.setTag(KnownTags.PEER_IPV4_ID, ip);
+ } else if (address instanceof Inet6Address) {
+ span.setTag(KnownTags.PEER_IPV6_ID, ip);
+ }
+ }
+}
diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/PeerSocketAddress.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/PeerSocketAddress.java
new file mode 100644
index 00000000000..3a77831775d
--- /dev/null
+++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/PeerSocketAddress.java
@@ -0,0 +1,30 @@
+package datadog.trace.bootstrap.instrumentation.decorator;
+
+import datadog.trace.api.KnownTags;
+import datadog.trace.bootstrap.instrumentation.api.SpanData;
+import datadog.trace.bootstrap.instrumentation.api.TagExtractor;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+
+/**
+ * Extracts the peer tags of a remote {@link InetSocketAddress}: those of its address (see {@link
+ * PeerAddress}), when it has one, and {@code peer.port}, when set.
+ */
+public final class PeerSocketAddress implements TagExtractor {
+ public static final PeerSocketAddress INSTANCE = new PeerSocketAddress();
+
+ private PeerSocketAddress() {}
+
+ @Override
+ public void extract(InetSocketAddress socketAddress, SpanData span) {
+ // an unresolved socket address is exactly one with no address
+ InetAddress address = socketAddress.getAddress();
+ if (address != null) {
+ PeerAddress.RESOLVED.extract(address, span);
+ }
+ int port = socketAddress.getPort();
+ if (port > BaseDecorator.UNSET_PORT) {
+ span.setTag(KnownTags.PEER_PORT_ID, port);
+ }
+ }
+}
diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/UrlConnectionDecorator.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/UrlConnectionDecorator.java
index e606481c3e4..e132923ac6b 100644
--- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/UrlConnectionDecorator.java
+++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/instrumentation/decorator/UrlConnectionDecorator.java
@@ -7,6 +7,7 @@
import datadog.trace.api.naming.SpanNaming;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes;
+import datadog.trace.bootstrap.instrumentation.api.SpanData;
import datadog.trace.bootstrap.instrumentation.api.TagExtractor;
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
import datadog.trace.bootstrap.instrumentation.api.http.OtelClientUrl;
@@ -73,7 +74,7 @@ public CharSequence operationName(@Nonnull final String protocol) {
private static final class FullUrl implements TagExtractor {
@Override
- public void extract(URI uri, AgentSpan span) {
+ public void extract(URI uri, SpanData span) {
// URLConnection historically records the original URL, including its query and fragment.
span.setTag(KnownTags.HTTP_URL_ID, uri.toString());
}
diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/PeerAddressTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/PeerAddressTest.java
new file mode 100644
index 00000000000..a0083183716
--- /dev/null
+++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/PeerAddressTest.java
@@ -0,0 +1,96 @@
+package datadog.trace.bootstrap.instrumentation.decorator;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import datadog.trace.api.KnownTags;
+import datadog.trace.bootstrap.instrumentation.api.SpanData;
+import datadog.trace.test.junit.utils.config.WithConfig;
+import datadog.trace.test.junit.utils.config.WithConfigExtension;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+// registered for the class so the one method-level @WithConfig is undone after that method
+@ExtendWith(WithConfigExtension.class)
+class PeerAddressTest {
+ private final SpanData span = mock(SpanData.class);
+
+ // getByAddress pairs a hostname with an IP without a DNS lookup
+ private static InetAddress ipv4() throws Exception {
+ return InetAddress.getByAddress("db.example", new byte[] {10, 0, 0, 1});
+ }
+
+ private static InetAddress ipv6() throws Exception {
+ byte[] ip = new byte[16];
+ ip[15] = 1;
+ return InetAddress.getByAddress("db6.example", ip);
+ }
+
+ @Test
+ void resolvedIpv4SetsHostnameAndIpv4() throws Exception {
+ PeerAddress.RESOLVED.extract(ipv4(), span);
+
+ verify(span).setTag(KnownTags.PEER_HOSTNAME_ID, (CharSequence) "db.example");
+ verify(span).setTag(KnownTags.PEER_IPV4_ID, (CharSequence) "10.0.0.1");
+ verifyNoMoreInteractions(span);
+ }
+
+ @Test
+ void resolvedIpv6SetsHostnameAndIpv6() throws Exception {
+ InetAddress address = ipv6();
+ PeerAddress.RESOLVED.extract(address, span);
+
+ verify(span).setTag(KnownTags.PEER_HOSTNAME_ID, (CharSequence) "db6.example");
+ verify(span).setTag(KnownTags.PEER_IPV6_ID, (CharSequence) address.getHostAddress());
+ verifyNoMoreInteractions(span);
+ }
+
+ @Test
+ void unresolvedSkipsHostname() throws Exception {
+ PeerAddress.UNRESOLVED.extract(ipv4(), span);
+
+ verify(span).setTag(KnownTags.PEER_IPV4_ID, (CharSequence) "10.0.0.1");
+ verifyNoMoreInteractions(span);
+ }
+
+ @Test
+ @WithConfig(key = "trace.peer.hostname.enabled", value = "false")
+ void hostnameDisabledSkipsHostname() throws Exception {
+ PeerAddress.RESOLVED.extract(ipv4(), span);
+
+ verify(span).setTag(KnownTags.PEER_IPV4_ID, (CharSequence) "10.0.0.1");
+ verifyNoMoreInteractions(span);
+ }
+
+ @Test
+ void socketAddressAddsPort() throws Exception {
+ PeerSocketAddress.INSTANCE.extract(new InetSocketAddress(ipv4(), 5432), span);
+
+ verify(span).setTag(KnownTags.PEER_HOSTNAME_ID, (CharSequence) "db.example");
+ verify(span).setTag(KnownTags.PEER_IPV4_ID, (CharSequence) "10.0.0.1");
+ verify(span).setTag(KnownTags.PEER_PORT_ID, 5432);
+ verifyNoMoreInteractions(span);
+ }
+
+ @Test
+ void unresolvedSocketAddressSetsOnlyPort() {
+ PeerSocketAddress.INSTANCE.extract(
+ InetSocketAddress.createUnresolved("bad.address.local", 999), span);
+
+ verify(span).setTag(KnownTags.PEER_PORT_ID, 999);
+ verifyNoMoreInteractions(span);
+ }
+
+ @Test
+ void unsetPortIsSkipped() throws Exception {
+ PeerSocketAddress.INSTANCE.extract(new InetSocketAddress(ipv4(), 0), span);
+
+ verify(span, never()).setTag(eq(KnownTags.PEER_PORT_ID), anyInt());
+ }
+}
diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/UrlConnectionDecoratorUrlTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/UrlConnectionDecoratorUrlTest.java
index e621e1bdf2a..710b27a2f14 100644
--- a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/UrlConnectionDecoratorUrlTest.java
+++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/instrumentation/decorator/UrlConnectionDecoratorUrlTest.java
@@ -1,7 +1,7 @@
package datadog.trace.bootstrap.instrumentation.decorator;
import static datadog.trace.api.config.GeneralConfig.TRACE_OTEL_SEMANTICS_ENABLED;
-import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
@@ -24,7 +24,8 @@ void datadogUrlConnectionKeepsOriginalUrl() {
UrlConnectionDecorator.DECORATE.onURI(span, URI.create(url));
- verify(span).setTag(KnownTags.HTTP_URL_ID, url);
- verify(span, never()).setTag(eq(KnownTags.HTTP_QUERY_STRING_ID), anyString());
+ // the URL extractors set tags through SpanData, whose id setters take a CharSequence
+ verify(span).setTag(KnownTags.HTTP_URL_ID, (CharSequence) url);
+ verify(span, never()).setTag(eq(KnownTags.HTTP_QUERY_STRING_ID), any(CharSequence.class));
}
}
diff --git a/dd-java-agent/instrumentation/kafka/kafka-clients-0.11/src/main/java/datadog/trace/instrumentation/kafka_clients/KafkaConsumerInstrumentation.java b/dd-java-agent/instrumentation/kafka/kafka-clients-0.11/src/main/java/datadog/trace/instrumentation/kafka_clients/KafkaConsumerInstrumentation.java
index 756f59aad4b..b4f4b2608a3 100644
--- a/dd-java-agent/instrumentation/kafka/kafka-clients-0.11/src/main/java/datadog/trace/instrumentation/kafka_clients/KafkaConsumerInstrumentation.java
+++ b/dd-java-agent/instrumentation/kafka/kafka-clients-0.11/src/main/java/datadog/trace/instrumentation/kafka_clients/KafkaConsumerInstrumentation.java
@@ -66,6 +66,7 @@ public String[] helperClassNames() {
packageName + ".TextMapInjectAdapterInterface",
packageName + ".KafkaConsumerInfo",
packageName + ".KafkaConsumerInstrumentationHelper",
+ packageName + ".KafkaConsumerTags",
packageName + ".KafkaDecorator",
packageName + ".TextMapExtractAdapter",
packageName + ".TracingIterableDelegator",
diff --git a/dd-java-agent/instrumentation/kafka/kafka-clients-0.11/src/main/java/datadog/trace/instrumentation/kafka_clients/KafkaConsumerTags.java b/dd-java-agent/instrumentation/kafka/kafka-clients-0.11/src/main/java/datadog/trace/instrumentation/kafka_clients/KafkaConsumerTags.java
new file mode 100644
index 00000000000..3f885fcd1e1
--- /dev/null
+++ b/dd-java-agent/instrumentation/kafka/kafka-clients-0.11/src/main/java/datadog/trace/instrumentation/kafka_clients/KafkaConsumerTags.java
@@ -0,0 +1,56 @@
+package datadog.trace.instrumentation.kafka_clients;
+
+import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.CONSUMER_GROUP;
+import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.KAFKA_BOOTSTRAP_SERVERS;
+import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.KAFKA_CLUSTER_ID;
+import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.MESSAGING_DESTINATION_NAME;
+
+import datadog.trace.bootstrap.instrumentation.api.FrozenTags;
+import datadog.trace.bootstrap.instrumentation.api.SpanData;
+import datadog.trace.bootstrap.instrumentation.api.TagExtractor;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+
+/**
+ * The consumer tags every record of one topic in one poll shares -- destination, group, cluster id
+ * and bootstrap servers -- extracted from the topic and frozen once, then applied to each record's
+ * span. Records come out of a poll partition by partition, so the topic rarely changes between
+ * records and one cached topic suffices. Used by one iterator, on one thread.
+ */
+public final class KafkaConsumerTags implements TagExtractor {
+ private final String group;
+ private final String clusterId;
+ private final String bootstrapServers;
+
+ private String frozenTopic;
+ private FrozenTags frozen;
+
+ public KafkaConsumerTags(String group, String clusterId, String bootstrapServers) {
+ this.group = group;
+ this.clusterId = clusterId;
+ this.bootstrapServers = bootstrapServers;
+ }
+
+ /** The frozen tags for {@code record}'s topic, frozen when the topic changes. */
+ public FrozenTags of(final ConsumerRecord, ?> record) {
+ final String topic = KafkaDecorator.topicOf(record);
+ if (topic != frozenTopic && !topic.equals(frozenTopic)) {
+ frozen = FrozenTags.of(topic, this);
+ frozenTopic = topic;
+ }
+ return frozen;
+ }
+
+ @Override
+ public void extract(final String topic, final SpanData span) {
+ span.setTag(MESSAGING_DESTINATION_NAME, topic);
+ if (group != null) {
+ span.setTag(CONSUMER_GROUP, group);
+ }
+ if (clusterId != null) {
+ span.setTag(KAFKA_CLUSTER_ID, clusterId);
+ }
+ if (bootstrapServers != null) {
+ span.setTag(KAFKA_BOOTSTRAP_SERVERS, bootstrapServers);
+ }
+ }
+}
diff --git a/dd-java-agent/instrumentation/kafka/kafka-clients-0.11/src/main/java/datadog/trace/instrumentation/kafka_clients/KafkaDecorator.java b/dd-java-agent/instrumentation/kafka/kafka-clients-0.11/src/main/java/datadog/trace/instrumentation/kafka_clients/KafkaDecorator.java
index 53c579a4dee..5d5023eb778 100644
--- a/dd-java-agent/instrumentation/kafka/kafka-clients-0.11/src/main/java/datadog/trace/instrumentation/kafka_clients/KafkaDecorator.java
+++ b/dd-java-agent/instrumentation/kafka/kafka-clients-0.11/src/main/java/datadog/trace/instrumentation/kafka_clients/KafkaDecorator.java
@@ -1,6 +1,5 @@
package datadog.trace.instrumentation.kafka_clients;
-import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.CONSUMER_GROUP;
import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.KAFKA_BOOTSTRAP_SERVERS;
import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.KAFKA_CLUSTER_ID;
import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.MESSAGING_DESTINATION_NAME;
@@ -16,6 +15,7 @@
import datadog.trace.api.cache.DDCaches;
import datadog.trace.api.naming.SpanNaming;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import datadog.trace.bootstrap.instrumentation.api.FrozenTags;
import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes;
import datadog.trace.bootstrap.instrumentation.api.Tags;
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
@@ -114,27 +114,23 @@ protected String spanKind() {
return spanKind;
}
+ /** {@code record}'s topic, or {@code "kafka"} when it has none. */
+ static String topicOf(final ConsumerRecord, ?> record) {
+ return record.topic() == null ? "kafka" : record.topic();
+ }
+
+ /**
+ * Decorates a consumed record's span. {@code consumerTags} are the destination, group, cluster id
+ * and bootstrap servers, frozen once per topic per poll ({@link KafkaConsumerTags}).
+ */
public void onConsume(
- final AgentSpan span,
- final ConsumerRecord record,
- String consumerGroup,
- String clusterId,
- String bootstrapServers) {
+ final AgentSpan span, final ConsumerRecord record, final FrozenTags consumerTags) {
if (record != null) {
- final String topic = record.topic() == null ? "kafka" : record.topic();
+ final String topic = topicOf(record);
span.setResourceName(CONSUMER_RESOURCE_NAME_CACHE.computeIfAbsent(topic, CONSUMER_PREFIX));
span.setTag(PARTITION, record.partition());
span.setTag(OFFSET, record.offset());
- span.setTag(MESSAGING_DESTINATION_NAME, topic);
- if (consumerGroup != null) {
- span.setTag(CONSUMER_GROUP, consumerGroup);
- }
- if (clusterId != null) {
- span.setTag(KAFKA_CLUSTER_ID, clusterId);
- }
- if (bootstrapServers != null) {
- span.setTag(KAFKA_BOOTSTRAP_SERVERS, bootstrapServers);
- }
+ span.setTagsFrom(consumerTags);
// TODO - do we really need both? This mechanism already adds a lot of... baggage.
// check to not record a duration if the message was sent from an old Kafka client
if (record.timestampType() != TimestampType.NO_TIMESTAMP_TYPE) {
diff --git a/dd-java-agent/instrumentation/kafka/kafka-clients-0.11/src/main/java/datadog/trace/instrumentation/kafka_clients/TracingIterator.java b/dd-java-agent/instrumentation/kafka/kafka-clients-0.11/src/main/java/datadog/trace/instrumentation/kafka_clients/TracingIterator.java
index 93179e4e3f2..2339192cee6 100644
--- a/dd-java-agent/instrumentation/kafka/kafka-clients-0.11/src/main/java/datadog/trace/instrumentation/kafka_clients/TracingIterator.java
+++ b/dd-java-agent/instrumentation/kafka/kafka-clients-0.11/src/main/java/datadog/trace/instrumentation/kafka_clients/TracingIterator.java
@@ -46,7 +46,9 @@ public class TracingIterator implements Iterator> {
private final KafkaDecorator decorator;
private final String group;
private final String clusterId;
- private final String bootstrapServers;
+
+ /** This poll's consumer tags, frozen per topic. */
+ private final KafkaConsumerTags consumerTags;
public TracingIterator(
final Iterator> delegateIterator,
@@ -60,7 +62,7 @@ public TracingIterator(
this.decorator = decorator;
this.group = group;
this.clusterId = clusterId;
- this.bootstrapServers = bootstrapServers;
+ this.consumerTags = new KafkaConsumerTags(group, clusterId, bootstrapServers);
}
@Override
@@ -146,7 +148,7 @@ protected void startNewRecordSpan(ConsumerRecord, ?> val) {
span.setTag(InstrumentationTags.TOMBSTONE, true);
}
decorator.afterStart(span);
- decorator.onConsume(span, val, group, clusterId, bootstrapServers);
+ decorator.onConsume(span, val, consumerTags.of(val));
if (InstrumenterConfig.get().isLegacyContextManagerEnabled()) {
activateNext(span);
} else {
diff --git a/dd-java-agent/instrumentation/kafka/kafka-clients-0.11/src/test/java/datadog/trace/instrumentation/kafka_clients/KafkaConsumerTagsTest.java b/dd-java-agent/instrumentation/kafka/kafka-clients-0.11/src/test/java/datadog/trace/instrumentation/kafka_clients/KafkaConsumerTagsTest.java
new file mode 100644
index 00000000000..b22a69c73dc
--- /dev/null
+++ b/dd-java-agent/instrumentation/kafka/kafka-clients-0.11/src/test/java/datadog/trace/instrumentation/kafka_clients/KafkaConsumerTagsTest.java
@@ -0,0 +1,66 @@
+package datadog.trace.instrumentation.kafka_clients;
+
+import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.CONSUMER_GROUP;
+import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.KAFKA_BOOTSTRAP_SERVERS;
+import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.KAFKA_CLUSTER_ID;
+import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.MESSAGING_DESTINATION_NAME;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import datadog.trace.api.TagMap;
+import datadog.trace.bootstrap.instrumentation.api.FrozenTags;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.junit.jupiter.api.Test;
+
+class KafkaConsumerTagsTest {
+ private static ConsumerRecord record(String topic, long offset) {
+ return new ConsumerRecord<>(topic, 0, offset, "key", "value");
+ }
+
+ @Test
+ void freezesTheConsumerTagsOfATopic() {
+ KafkaConsumerTags tags = new KafkaConsumerTags("group", "cluster", "broker:9092");
+
+ TagMap frozen = tags.of(record("orders", 0)).tags();
+
+ assertEquals(4, frozen.size());
+ assertEquals("orders", frozen.getObject(MESSAGING_DESTINATION_NAME));
+ assertEquals("group", frozen.getObject(CONSUMER_GROUP));
+ assertEquals("cluster", frozen.getObject(KAFKA_CLUSTER_ID));
+ assertEquals("broker:9092", frozen.getObject(KAFKA_BOOTSTRAP_SERVERS));
+ }
+
+ @Test
+ void recordsOfTheSameTopicShareOneFreeze() {
+ KafkaConsumerTags tags = new KafkaConsumerTags("group", "cluster", "broker:9092");
+
+ FrozenTags first = tags.of(record("orders", 0));
+ // an equal but distinct topic string, as a later fetch may hand out
+ assertSame(first, tags.of(record(new String("orders"), 1)));
+ }
+
+ @Test
+ void aNewTopicFreezesAgain() {
+ KafkaConsumerTags tags = new KafkaConsumerTags("group", "cluster", "broker:9092");
+
+ FrozenTags orders = tags.of(record("orders", 0));
+ FrozenTags payments = tags.of(record("payments", 0));
+
+ assertNotSame(orders, payments);
+ assertEquals("payments", payments.tags().getObject(MESSAGING_DESTINATION_NAME));
+ assertEquals(
+ "orders", tags.of(record("orders", 1)).tags().getObject(MESSAGING_DESTINATION_NAME));
+ }
+
+ @Test
+ void missingConsumerInfoIsLeftOut() {
+ KafkaConsumerTags tags = new KafkaConsumerTags(null, null, null);
+
+ TagMap frozen = tags.of(record("orders", 0)).tags();
+
+ assertEquals(1, frozen.size());
+ assertNull(frozen.getObject(CONSUMER_GROUP));
+ }
+}
diff --git a/dd-java-agent/instrumentation/kafka/kafka-clients-3.8/src/main/java/datadog/trace/instrumentation/kafka_clients38/KafkaConsumerInstrumentation.java b/dd-java-agent/instrumentation/kafka/kafka-clients-3.8/src/main/java/datadog/trace/instrumentation/kafka_clients38/KafkaConsumerInstrumentation.java
index 70f3f6dbc92..4da2b5be2a6 100644
--- a/dd-java-agent/instrumentation/kafka/kafka-clients-3.8/src/main/java/datadog/trace/instrumentation/kafka_clients38/KafkaConsumerInstrumentation.java
+++ b/dd-java-agent/instrumentation/kafka/kafka-clients-3.8/src/main/java/datadog/trace/instrumentation/kafka_clients38/KafkaConsumerInstrumentation.java
@@ -54,6 +54,7 @@ public String[] helperClassNames() {
packageName + ".TextMapInjectAdapterInterface",
packageName + ".KafkaConsumerInfo",
packageName + ".KafkaConsumerInstrumentationHelper",
+ packageName + ".KafkaConsumerTags",
packageName + ".KafkaDecorator",
packageName + ".TextMapExtractAdapter",
packageName + ".TracingIterableDelegator",
diff --git a/dd-java-agent/instrumentation/kafka/kafka-clients-3.8/src/main/java17/datadog/trace/instrumentation/kafka_clients38/KafkaConsumerTags.java b/dd-java-agent/instrumentation/kafka/kafka-clients-3.8/src/main/java17/datadog/trace/instrumentation/kafka_clients38/KafkaConsumerTags.java
new file mode 100644
index 00000000000..70eeecbd772
--- /dev/null
+++ b/dd-java-agent/instrumentation/kafka/kafka-clients-3.8/src/main/java17/datadog/trace/instrumentation/kafka_clients38/KafkaConsumerTags.java
@@ -0,0 +1,56 @@
+package datadog.trace.instrumentation.kafka_clients38;
+
+import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.CONSUMER_GROUP;
+import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.KAFKA_BOOTSTRAP_SERVERS;
+import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.KAFKA_CLUSTER_ID;
+import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.MESSAGING_DESTINATION_NAME;
+
+import datadog.trace.bootstrap.instrumentation.api.FrozenTags;
+import datadog.trace.bootstrap.instrumentation.api.SpanData;
+import datadog.trace.bootstrap.instrumentation.api.TagExtractor;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+
+/**
+ * The consumer tags every record of one topic in one poll shares -- destination, group, cluster id
+ * and bootstrap servers -- extracted from the topic and frozen once, then applied to each record's
+ * span. Records come out of a poll partition by partition, so the topic rarely changes between
+ * records and one cached topic suffices. Used by one iterator, on one thread.
+ */
+public final class KafkaConsumerTags implements TagExtractor {
+ private final String group;
+ private final String clusterId;
+ private final String bootstrapServers;
+
+ private String frozenTopic;
+ private FrozenTags frozen;
+
+ public KafkaConsumerTags(String group, String clusterId, String bootstrapServers) {
+ this.group = group;
+ this.clusterId = clusterId;
+ this.bootstrapServers = bootstrapServers;
+ }
+
+ /** The frozen tags for {@code record}'s topic, frozen when the topic changes. */
+ public FrozenTags of(final ConsumerRecord, ?> record) {
+ final String topic = KafkaDecorator.topicOf(record);
+ if (topic != frozenTopic && !topic.equals(frozenTopic)) {
+ frozen = FrozenTags.of(topic, this);
+ frozenTopic = topic;
+ }
+ return frozen;
+ }
+
+ @Override
+ public void extract(final String topic, final SpanData span) {
+ span.setTag(MESSAGING_DESTINATION_NAME, topic);
+ if (group != null) {
+ span.setTag(CONSUMER_GROUP, group);
+ }
+ if (clusterId != null) {
+ span.setTag(KAFKA_CLUSTER_ID, clusterId);
+ }
+ if (bootstrapServers != null) {
+ span.setTag(KAFKA_BOOTSTRAP_SERVERS, bootstrapServers);
+ }
+ }
+}
diff --git a/dd-java-agent/instrumentation/kafka/kafka-clients-3.8/src/main/java17/datadog/trace/instrumentation/kafka_clients38/KafkaDecorator.java b/dd-java-agent/instrumentation/kafka/kafka-clients-3.8/src/main/java17/datadog/trace/instrumentation/kafka_clients38/KafkaDecorator.java
index d2d6f53b8a9..d09533cd8a2 100644
--- a/dd-java-agent/instrumentation/kafka/kafka-clients-3.8/src/main/java17/datadog/trace/instrumentation/kafka_clients38/KafkaDecorator.java
+++ b/dd-java-agent/instrumentation/kafka/kafka-clients-3.8/src/main/java17/datadog/trace/instrumentation/kafka_clients38/KafkaDecorator.java
@@ -1,6 +1,5 @@
package datadog.trace.instrumentation.kafka_clients38;
-import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.CONSUMER_GROUP;
import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.KAFKA_BOOTSTRAP_SERVERS;
import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.KAFKA_CLUSTER_ID;
import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.MESSAGING_DESTINATION_NAME;
@@ -16,6 +15,7 @@
import datadog.trace.api.cache.DDCaches;
import datadog.trace.api.naming.SpanNaming;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import datadog.trace.bootstrap.instrumentation.api.FrozenTags;
import datadog.trace.bootstrap.instrumentation.api.InternalSpanTypes;
import datadog.trace.bootstrap.instrumentation.api.Tags;
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
@@ -114,27 +114,23 @@ protected String spanKind() {
return spanKind;
}
+ /** {@code record}'s topic, or {@code "kafka"} when it has none. */
+ static String topicOf(final ConsumerRecord, ?> record) {
+ return record.topic() == null ? "kafka" : record.topic();
+ }
+
+ /**
+ * Decorates a consumed record's span. {@code consumerTags} are the destination, group, cluster id
+ * and bootstrap servers, frozen once per topic per poll ({@link KafkaConsumerTags}).
+ */
public void onConsume(
- final AgentSpan span,
- final ConsumerRecord record,
- String consumerGroup,
- String clusterId,
- String bootstrapServers) {
+ final AgentSpan span, final ConsumerRecord record, final FrozenTags consumerTags) {
if (record != null) {
- final String topic = record.topic() == null ? "kafka" : record.topic();
+ final String topic = topicOf(record);
span.setResourceName(CONSUMER_RESOURCE_NAME_CACHE.computeIfAbsent(topic, CONSUMER_PREFIX));
span.setTag(PARTITION, record.partition());
span.setTag(OFFSET, record.offset());
- span.setTag(MESSAGING_DESTINATION_NAME, topic);
- if (consumerGroup != null) {
- span.setTag(CONSUMER_GROUP, consumerGroup);
- }
- if (clusterId != null) {
- span.setTag(KAFKA_CLUSTER_ID, clusterId);
- }
- if (bootstrapServers != null) {
- span.setTag(KAFKA_BOOTSTRAP_SERVERS, bootstrapServers);
- }
+ span.setTagsFrom(consumerTags);
// TODO - do we really need both? This mechanism already adds a lot of... baggage.
// check to not record a duration if the message was sent from an old Kafka client
if (record.timestampType() != TimestampType.NO_TIMESTAMP_TYPE) {
diff --git a/dd-java-agent/instrumentation/kafka/kafka-clients-3.8/src/main/java17/datadog/trace/instrumentation/kafka_clients38/TracingIterator.java b/dd-java-agent/instrumentation/kafka/kafka-clients-3.8/src/main/java17/datadog/trace/instrumentation/kafka_clients38/TracingIterator.java
index 7beba848473..d92426ae035 100644
--- a/dd-java-agent/instrumentation/kafka/kafka-clients-3.8/src/main/java17/datadog/trace/instrumentation/kafka_clients38/TracingIterator.java
+++ b/dd-java-agent/instrumentation/kafka/kafka-clients-3.8/src/main/java17/datadog/trace/instrumentation/kafka_clients38/TracingIterator.java
@@ -42,7 +42,9 @@ public class TracingIterator implements Iterator> {
private final KafkaDecorator decorator;
private final String group;
private final String clusterId;
- private final String bootstrapServers;
+
+ /** This poll's consumer tags, frozen per topic. */
+ private final KafkaConsumerTags consumerTags;
public TracingIterator(
final Iterator> delegateIterator,
@@ -56,7 +58,7 @@ public TracingIterator(
this.decorator = decorator;
this.group = group;
this.clusterId = clusterId;
- this.bootstrapServers = bootstrapServers;
+ this.consumerTags = new KafkaConsumerTags(group, clusterId, bootstrapServers);
}
@Override
@@ -142,7 +144,7 @@ protected void startNewRecordSpan(ConsumerRecord, ?> val) {
span.setTag(InstrumentationTags.TOMBSTONE, true);
}
decorator.afterStart(span);
- decorator.onConsume(span, val, group, clusterId, bootstrapServers);
+ decorator.onConsume(span, val, consumerTags.of(val));
if (InstrumenterConfig.get().isLegacyContextManagerEnabled()) {
activateNext(span);
} else {
diff --git a/dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/client/HttpClientRequestTracingHandler.java b/dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/client/HttpClientRequestTracingHandler.java
index 47134f4a0cf..c61ed2d7d37 100644
--- a/dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/client/HttpClientRequestTracingHandler.java
+++ b/dd-java-agent/instrumentation/netty/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/client/HttpClientRequestTracingHandler.java
@@ -6,6 +6,8 @@
import static datadog.trace.instrumentation.netty41.AttributeKeys.CLIENT_PARENT_ATTRIBUTE_KEY;
import static datadog.trace.instrumentation.netty41.AttributeKeys.CONNECT_PARENT_CONTINUATION_ATTRIBUTE_KEY;
import static datadog.trace.instrumentation.netty41.AttributeKeys.CONTEXT_ATTRIBUTE_KEY;
+import static datadog.trace.instrumentation.netty41.AttributeKeys.PEER_TAGS_ADDRESS_ATTRIBUTE_KEY;
+import static datadog.trace.instrumentation.netty41.AttributeKeys.PEER_TAGS_ATTRIBUTE_KEY;
import static datadog.trace.instrumentation.netty41.client.NettyHttpClientDecorator.DECORATE;
import static datadog.trace.instrumentation.netty41.client.NettyHttpClientDecorator.DECORATE_SECURE;
import static datadog.trace.instrumentation.netty41.client.NettyHttpClientDecorator.NETTY_CLIENT;
@@ -17,12 +19,15 @@
import datadog.context.ContextScope;
import datadog.trace.api.Config;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import datadog.trace.bootstrap.instrumentation.api.FrozenTags;
+import datadog.trace.bootstrap.instrumentation.decorator.PeerSocketAddress;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.http.HttpRequest;
+import io.netty.util.Attribute;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
@@ -84,10 +89,7 @@ public void write(final ChannelHandlerContext ctx, final Object msg, final Chann
decorate.afterStart(span);
decorate.onRequest(span, request);
- SocketAddress socketAddress = ctx.channel().remoteAddress();
- if (socketAddress instanceof InetSocketAddress) {
- decorate.onPeerConnection(span, (InetSocketAddress) socketAddress);
- }
+ span.setTagsFrom(peerTags(ctx.channel()));
// AWS calls are often signed, so we can't add headers without breaking the signature.
if (!awsClientCall) {
@@ -111,6 +113,33 @@ public void write(final ChannelHandlerContext ctx, final Object msg, final Chann
}
}
+ /**
+ * The peer tags of {@code channel}'s remote address, frozen once per address and kept on the
+ * channel, so later requests skip rebuilding the IP string and resolving the hostname. An HTTP/2
+ * stream reports its connection's address object, so its tags are kept on the connection and
+ * shared by its streams; any other parent (a server channel) reports a different address, so the
+ * channel keeps its own. The cached tags are reused only while the channel still reports the
+ * address object they were frozen from. Nothing is cached while there is no remote address yet.
+ * Called on the channel's event loop, which HTTP/2 streams share with their connection.
+ */
+ private static FrozenTags peerTags(final Channel channel) {
+ final SocketAddress address = channel.remoteAddress();
+ if (!(address instanceof InetSocketAddress)) {
+ return FrozenTags.EMPTY;
+ }
+ final Channel parent = channel.parent();
+ final Channel holder = parent != null && parent.remoteAddress() == address ? parent : channel;
+ final Attribute tagsAttribute = holder.attr(PEER_TAGS_ATTRIBUTE_KEY);
+ final Attribute addressAttribute = holder.attr(PEER_TAGS_ADDRESS_ATTRIBUTE_KEY);
+ FrozenTags tags = tagsAttribute.get();
+ if (tags == null || addressAttribute.get() != address) {
+ tags = FrozenTags.of((InetSocketAddress) address, PeerSocketAddress.INSTANCE);
+ tagsAttribute.set(tags);
+ addressAttribute.set(address);
+ }
+ return tags;
+ }
+
private static ContextContinuation takeConnectParentContinuation(
final ChannelHandlerContext ctx) {
final Channel channel = ctx.channel();
diff --git a/dd-java-agent/instrumentation/netty/netty-common/src/main/java/datadog/trace/instrumentation/netty41/AttributeKeys.java b/dd-java-agent/instrumentation/netty/netty-common/src/main/java/datadog/trace/instrumentation/netty41/AttributeKeys.java
index addf40b55d6..d70aafc2226 100644
--- a/dd-java-agent/instrumentation/netty/netty-common/src/main/java/datadog/trace/instrumentation/netty41/AttributeKeys.java
+++ b/dd-java-agent/instrumentation/netty/netty-common/src/main/java/datadog/trace/instrumentation/netty41/AttributeKeys.java
@@ -6,8 +6,10 @@
import datadog.context.ContextContinuation;
import datadog.trace.api.GenericClassValue;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import datadog.trace.bootstrap.instrumentation.api.FrozenTags;
import datadog.trace.bootstrap.instrumentation.websocket.HandlerContext;
import io.netty.util.AttributeKey;
+import java.net.SocketAddress;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -28,6 +30,14 @@ public final class AttributeKeys {
public static final AttributeKey HTTP2_CONNECTION_CODEC_ATTRIBUTE_KEY =
attributeKey("datadog.http2.connection.codec");
+ /** A client channel's peer tags, frozen from its remote address. */
+ public static final AttributeKey PEER_TAGS_ATTRIBUTE_KEY =
+ attributeKey("datadog.client.peer.tags");
+
+ /** The remote address {@link #PEER_TAGS_ATTRIBUTE_KEY}'s tags were frozen from. */
+ public static final AttributeKey PEER_TAGS_ADDRESS_ATTRIBUTE_KEY =
+ attributeKey("datadog.client.peer.tags.address");
+
public static final AttributeKey PARENT_CONTEXT_ATTRIBUTE_KEY =
attributeKey("datadog.server.parent-context");
diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-0.3/src/test/groovy/TypeConverterTest.groovy b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-0.3/src/test/groovy/TypeConverterTest.groovy
index 60c0f43d9be..9dcf709d75f 100644
--- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-0.3/src/test/groovy/TypeConverterTest.groovy
+++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-0.3/src/test/groovy/TypeConverterTest.groovy
@@ -53,7 +53,6 @@ class TypeConverterTest extends InstrumentationSpecification {
[:],
false,
"fakeType",
- 0,
trace,
null,
null,
diff --git a/dd-java-agent/instrumentation/opentracing/opentracing-0.31/src/test/groovy/TypeConverterTest.groovy b/dd-java-agent/instrumentation/opentracing/opentracing-0.31/src/test/groovy/TypeConverterTest.groovy
index 3a3b5c6efca..db29f8de153 100644
--- a/dd-java-agent/instrumentation/opentracing/opentracing-0.31/src/test/groovy/TypeConverterTest.groovy
+++ b/dd-java-agent/instrumentation/opentracing/opentracing-0.31/src/test/groovy/TypeConverterTest.groovy
@@ -65,7 +65,6 @@ class TypeConverterTest extends InstrumentationSpecification {
[:],
false,
"fakeType",
- 0,
trace,
null,
null,
diff --git a/dd-java-agent/instrumentation/opentracing/opentracing-0.32/src/test/groovy/TypeConverterTest.groovy b/dd-java-agent/instrumentation/opentracing/opentracing-0.32/src/test/groovy/TypeConverterTest.groovy
index 9ecfd5221ef..1506f3eb983 100644
--- a/dd-java-agent/instrumentation/opentracing/opentracing-0.32/src/test/groovy/TypeConverterTest.groovy
+++ b/dd-java-agent/instrumentation/opentracing/opentracing-0.32/src/test/groovy/TypeConverterTest.groovy
@@ -65,7 +65,6 @@ class TypeConverterTest extends InstrumentationSpecification {
[:],
false,
"fakeType",
- 0,
trace,
null,
null,
diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java
index d31137c3aef..2fa5365f935 100644
--- a/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java
+++ b/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java
@@ -190,5 +190,11 @@ public final class TracerConfig {
public static final String TRACE_ORG_GUARD_STRICT = "trace.org.guard.strict";
public static final String TRACE_ORG_GUARD_TRUSTED_OPMS = "trace.org.guard.trusted.opms";
+ /**
+ * Stores known tags that have a dense-store slot in TagMap's dense store (no per-tag entry)
+ * instead of its hash buckets. Experimental, off by default.
+ */
+ public static final String TRACE_DENSE_TAGS_ENABLED = "trace.experimental.dense.tags.enabled";
+
private TracerConfig() {}
}
diff --git a/dd-trace-core/src/jmh/java/datadog/trace/common/writer/ddagent/TraceMapperBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/common/writer/ddagent/TraceMapperBenchmark.java
index 54192804b05..f7a6fec6e2c 100644
--- a/dd-trace-core/src/jmh/java/datadog/trace/common/writer/ddagent/TraceMapperBenchmark.java
+++ b/dd-trace-core/src/jmh/java/datadog/trace/common/writer/ddagent/TraceMapperBenchmark.java
@@ -116,7 +116,6 @@ public void init(Blackhole blackhole) {
Collections.emptyMap(),
false,
UTF8BytesString.create("type"),
- 0,
traceCollector,
null,
null,
diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/GetTagBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/GetTagBenchmark.java
new file mode 100644
index 00000000000..57ce57b404f
--- /dev/null
+++ b/dd-trace-core/src/jmh/java/datadog/trace/core/GetTagBenchmark.java
@@ -0,0 +1,116 @@
+package datadog.trace.core;
+
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
+import datadog.trace.api.KnownTags;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+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.TearDown;
+import org.openjdk.jmh.annotations.Warmup;
+
+/**
+ * Measures reading tags back from a live span, by how the tag is named: a known tag's name, a
+ * custom name, an OpenTelemetry name ({@code server.address}, which the core treats as a custom
+ * name), and misses. {@code getTag} takes the span's tag lock; {@code unsafeGetTag} reads without
+ * it, as the tracer's internal readers do.
+ */
+@State(Scope.Thread)
+@Warmup(iterations = 3, time = 1)
+@Measurement(iterations = 5, time = 1)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(NANOSECONDS)
+@Fork(value = 3)
+public class GetTagBenchmark {
+ static final CoreTracer TRACER = CoreTracer.builder().build();
+
+ private DDSpan span;
+
+ /**
+ * The known tags the span holds, read in turn by the {@code *_cycle} arms so a read's cost is
+ * averaged over the span's tags rather than decided by whether one particular tag collides in its
+ * bucket. A power-of-two length lets the index wrap with a mask.
+ */
+ private static final String[] KNOWN_HELD = {
+ "span.kind",
+ KnownTags.HTTP_ROUTE_NAME,
+ KnownTags.PEER_HOSTNAME_NAME,
+ KnownTags.HTTP_STATUS_CODE_NAME
+ };
+
+ private int next;
+
+ @Setup
+ public void setup() {
+ span = (DDSpan) TRACER.startSpan("benchmark", "get.tag");
+ span.setTag("span.kind", "client");
+ span.setTag(KnownTags.HTTP_ROUTE_NAME, "/users/{id}");
+ span.setTag(KnownTags.PEER_HOSTNAME_NAME, "db.example");
+ span.setTag(KnownTags.HTTP_STATUS_CODE_NAME, 200);
+ span.setTag("my.custom.tag", "custom");
+ span.setTag("my.other.tag", 42);
+ }
+
+ @TearDown
+ public void tearDown() {
+ span.finish();
+ }
+
+ @Benchmark
+ public Object knownTag_getTag() {
+ return span.getTag(KnownTags.HTTP_ROUTE_NAME);
+ }
+
+ @Benchmark
+ public Object customTag_getTag() {
+ return span.getTag("my.custom.tag");
+ }
+
+ @Benchmark
+ public Object aliasName_getTag() {
+ return span.getTag("server.address");
+ }
+
+ @Benchmark
+ public Object missingTag_getTag() {
+ return span.getTag("not.there");
+ }
+
+ /** A known tag the span doesn't hold: a clear slot bit can answer without a probe. */
+ @Benchmark
+ public Object missingKnownTag_getTag() {
+ return span.getTag(KnownTags.HTTP_METHOD_NAME);
+ }
+
+ @Benchmark
+ public Object missingKnownTag_unsafeGetTag() {
+ return span.unsafeGetTag(KnownTags.HTTP_METHOD_NAME);
+ }
+
+ @Benchmark
+ public Object knownTag_unsafeGetTag() {
+ return span.unsafeGetTag(KnownTags.HTTP_ROUTE_NAME);
+ }
+
+ /** Every known tag the span holds, in turn; see {@link #KNOWN_HELD}. */
+ @Benchmark
+ public Object knownTags_cycle_unsafeGetTag() {
+ return span.unsafeGetTag(KNOWN_HELD[next++ & (KNOWN_HELD.length - 1)]);
+ }
+
+ @Benchmark
+ public Object knownTags_cycle_getTag() {
+ return span.getTag(KNOWN_HELD[next++ & (KNOWN_HELD.length - 1)]);
+ }
+
+ @Benchmark
+ public Object customTag_unsafeGetTag() {
+ return span.unsafeGetTag("my.custom.tag");
+ }
+}
diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/HeaderPathBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/HeaderPathBenchmark.java
new file mode 100644
index 00000000000..e9e2d73b517
--- /dev/null
+++ b/dd-trace-core/src/jmh/java/datadog/trace/core/HeaderPathBenchmark.java
@@ -0,0 +1,232 @@
+package datadog.trace.core;
+
+import static datadog.context.Context.root;
+import static datadog.trace.bootstrap.instrumentation.api.AgentSpan.fromContext;
+import static java.util.concurrent.TimeUnit.MICROSECONDS;
+
+import datadog.context.Context;
+import datadog.context.ContextScope;
+import datadog.context.propagation.CarrierSetter;
+import datadog.context.propagation.CarrierVisitor;
+import datadog.context.propagation.Propagators;
+import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext;
+import datadog.trace.bootstrap.instrumentation.api.SpanPrototype;
+import datadog.trace.bootstrap.instrumentation.api.Tags;
+import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+
+/**
+ * One whole request per op, through the header code paths: extract the inbound headers with the
+ * default propagator, start the server span from what was extracted, run a few children, make one
+ * outbound HTTP call that injects its headers, then finish everything.
+ *
+ * The existing propagation benchmarks each measure one step (extracting into a context,
+ * injecting from a prebuilt one, parsing one header value). They can't see costs that the extracted
+ * state causes later, on every span of the trace: for example, a {@code tracestate} that leads each
+ * span to allocate sampling state. Measuring the whole request catches those.
+ *
+ *
{@code headers} selects the inbound headers, under the default propagation styles:
+ *
+ *
+ * - {@code none}: a new trace;
+ *
- {@code datadog}: {@code x-datadog-*}, with propagated tags;
+ *
- {@code tracecontext}: {@code traceparent} and a {@code tracestate} carrying {@code dd},
+ * OpenTelemetry ({@code ot}) and a third-party member;
+ *
- {@code both}: Datadog and W3C headers for the same trace, as a Datadog-to-Datadog call
+ * sends them;
+ *
- {@code bothBaggage}: {@code both}, plus a {@code baggage} header.
+ *
+ */
+@State(Scope.Benchmark)
+@Warmup(iterations = 5)
+@Measurement(iterations = 5)
+@BenchmarkMode(Mode.Throughput)
+@Threads(8)
+@OutputTimeUnit(MICROSECONDS)
+@Fork(value = 3, jvmArgsAppend = "-DTEST_LOG_LEVEL=warn")
+public class HeaderPathBenchmark {
+ static final int JDBC_SPANS = 3;
+
+ static final String TRACE_ID_HEX = "66a0f6c800000000a3ce929d0e0e4736";
+ static final String TRACE_ID_LOW = "11803532876627986230"; // a3ce929d0e0e4736
+ static final String PARENT_ID = "67667974448284343"; // 00f067aa0ba902b7
+ static final String PARENT_ID_HEX = "00f067aa0ba902b7";
+
+ static final String SERVER_INSTRUMENTATION = "tomcat-server";
+ static final CharSequence SERVER_OPERATION = UTF8BytesString.create("servlet.request");
+ static final String JDBC_INSTRUMENTATION = "java-jdbc-prepared_statement";
+ static final CharSequence JDBC_OPERATION = UTF8BytesString.create("database.query");
+ static final String CLIENT_INSTRUMENTATION = "okhttp";
+ static final CharSequence CLIENT_OPERATION = UTF8BytesString.create("okhttp.request");
+
+ static final CarrierVisitor