Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
eabce40
Bucket known tags by slot in TagMap
dougqh Oct 22, 2026
19b2e88
Add whole-span and whole-trace allocation benchmarks
dougqh Oct 22, 2026
6e65a31
Vary TraceShapeBenchmark's trace size; test bucketHash
dougqh Oct 22, 2026
d72b784
Merge branch 'master' into dougqh/tagmap-slot-buckets
dougqh Oct 22, 2026
2271e4f
Add a whole-request benchmark for the header code paths
dougqh Oct 22, 2026
fa9b441
Make a null tag value remove the tag in TagMap
dougqh Oct 23, 2026
7894411
Merge branch 'dougqh/tagmap-null-removes' into dougqh/tagmap-slot-buc…
dougqh Oct 23, 2026
8e57e87
Introduce SpanStorage for eager tag post-processors
dougqh Oct 23, 2026
3c91e7f
Attach HeaderPathBenchmark's server span through the local tracer
dougqh Oct 26, 2026
ae4ec9a
Merge branch 'dougqh/tagmap-slot-buckets' into dougqh/eager-postproce…
dougqh Oct 26, 2026
f1b60e4
Remove an earlier payload tag when a later value at its path is null
dougqh Oct 26, 2026
54ed705
Merge branch 'dougqh/tagmap-null-removes' into dougqh/tagmap-slot-buc…
dougqh Oct 26, 2026
e0b022c
Merge branch 'dougqh/tagmap-slot-buckets' into dougqh/eager-postproce…
dougqh Oct 26, 2026
884c508
Load and store known tags by id through SpanStorage
dougqh Oct 26, 2026
590f6e7
Test that a known tag's id and name address the same tag in SpanStorage
dougqh Oct 26, 2026
38dc4b3
Rename SpanStorage to TagStore
dougqh Oct 26, 2026
4540183
Apply suggestion from @sarahchen6
dougqh Oct 26, 2026
7739065
Say why the span implements TagStore
dougqh Oct 26, 2026
08b0645
Merge branch 'dougqh/tagmap-null-removes' into dougqh/tagmap-slot-buc…
dougqh Oct 26, 2026
f5fa140
Merge branch 'dougqh/tagmap-slot-buckets' into dougqh/eager-postproce…
dougqh Oct 26, 2026
a0f2786
Merge branch 'master' into dougqh/tagmap-null-removes
dougqh Oct 26, 2026
0c73aff
Merge branch 'dougqh/tagmap-null-removes' into dougqh/tagmap-slot-buc…
dougqh Oct 26, 2026
933a688
Merge branch 'dougqh/tagmap-slot-buckets' into dougqh/eager-postproce…
dougqh Oct 26, 2026
36bbbd2
Test that urlOrPath answers the same from a TagStore as from a map
dougqh Oct 26, 2026
b9dbfa9
Name the post-processor chain's TagStore parameter unsafeTags
dougqh Oct 26, 2026
3eed220
Rename TagStoreTest's storage helper and locals
dougqh Oct 26, 2026
710f8e9
Merge branch 'master' into dougqh/eager-postprocessors-span-context
dougqh Oct 27, 2026
cdfc195
Annotate TagStore's nullable values and keep implementation details o…
dougqh Oct 28, 2026
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 @@ -19,6 +19,7 @@
import datadog.trace.api.KnownTags;
import datadog.trace.api.ProcessTags;
import datadog.trace.api.TagMap;
import datadog.trace.api.TagStore;
import datadog.trace.api.cache.DDCache;
import datadog.trace.api.cache.DDCaches;
import datadog.trace.api.config.TracerConfig;
Expand Down Expand Up @@ -68,7 +69,7 @@
* the associated Span instance
*/
public class DDSpanContext
implements AgentSpanContext, RequestContext, TraceSegment, ProfilerContext {
implements AgentSpanContext, RequestContext, TraceSegment, ProfilerContext, TagStore {
private static final Logger log = LoggerFactory.getLogger(DDSpanContext.class);

public static final String PRIORITY_SAMPLING_KEY = "_sampling_priority_v1";
Expand Down Expand Up @@ -1395,6 +1396,50 @@ Object getTag(final String key) {
}
}

/*
* TagStore: the span's tags as stored, loaded and stored with no locking, interception or
* interpretation -- the storage beneath the span, as a Map was beneath a TagMap. A span isn't
* conceptually a TagStore. It implements one so post-processing is handed the span's storage
* without allocating: a wrapper would be free only if escape analysis removed it, and the
* post-processor chain's dispatch loop defeats that. Callers hold the span's tag lock, as
* post-processing does.
*/

@Override
public Object load(String tag) {
return unsafeTags.getObject(tag);
}

@Override
public String loadString(String tag) {
return unsafeTags.getString(tag);
}

@Override
public Object load(long tagId) {
return unsafeTags.load(tagId);
}

@Override
public String loadString(long tagId) {
return unsafeTags.loadString(tagId);
}

@Override
public void store(String tag, Object value) {
unsafeTags.set(tag, value);
}

@Override
public void store(long tagId, Object value) {
unsafeTags.set(tagId, value);
}

@Override
public void store(TagMap.EntryReader entry) {
unsafeTags.set(entry);
}

/**
* This is not thread-safe and must only be used when it can be guaranteed that the context will
* not be mutated. This is internal API and must not be exposed to users.
Expand Down Expand Up @@ -1500,7 +1545,7 @@ public <T> void setMetaStruct(final String field, final T value) {

void earlyProcessTags(AppendableSpanLinks links) {
synchronized (unsafeTags) {
TagsPostProcessorFactory.eagerProcessor().processTags(unsafeTags, this, links);
TagsPostProcessorFactory.eagerProcessor().processTags(this, this, links);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package datadog.trace.core.tagprocessor;

import datadog.trace.api.TagStore;
import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks;
import datadog.trace.core.DDSpanContext;
import java.util.Objects;
import javax.annotation.Nonnull;

/** Runs eager post-processors in order. */
public final class EagerPostProcessorChain extends EagerTagsPostProcessor {
private final EagerTagsPostProcessor[] chain;

public EagerPostProcessorChain(@Nonnull final EagerTagsPostProcessor... processors) {
chain = Objects.requireNonNull(processors);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❔ question: ‏Does it make sense to forbid null but allow empty array?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It mirrors the existing PostProcessorChain on master. An empty chain is a valid no-op (no processors enabled), while a null array is a programming error. In practice it's never empty: TagsPostProcessorFactory always adds PeerServiceCalculator.

Reply written by Claude (Claude Code) on behalf of dougqh.

}

@Override
public void processTags(
TagStore unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) {
for (final EagerTagsPostProcessor processor : chain) {
processor.processTags(unsafeTags, spanContext, spanLinks);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package datadog.trace.core.tagprocessor;

import datadog.trace.api.TagStore;
import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks;
import datadog.trace.core.DDSpanContext;

/**
* A tag post-processor run on the application thread as a span finishes. It works on the span's
* tags as stored, through a {@link TagStore}, rather than a {@code TagMap} the span would have to
* build for it. The span passes itself as that store, which allocates nothing.
*/
public abstract class EagerTagsPostProcessor {
/**
* Processes the span's tags.
*
* @param unsafeTags the span's tags as stored, to read and write; the caller holds the span's tag
* lock
* @param spanContext the span, for its other properties
* @param spanLinks the span's links
*/
public abstract void processTags(

@dougqh dougqh Oct 26, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this PR, DDSpanContext becomes TagStore, but I think we still want to keep them conceptually separate. I made that change because TagStore is direct to the underlying store without locking or extra interpretation -- just like Map & TagMap before.

The main reason that DDSpanComponent implements TagStore directly is to avoid allocation. Ideally, I think EA (escape analysis)-friendly wrapper could have been used, but the EagerPostProcessorChain dispatch loop isn't EA-friendly.

TagStore unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package datadog.trace.core.tagprocessor;

import static datadog.trace.api.KnownTags.HTTP_METHOD_ID;
import static datadog.trace.api.KnownTags.HTTP_ROUTE_ID;
import static datadog.trace.bootstrap.instrumentation.api.OtelHttpSemantics.urlOrPath;
import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_METHOD;
import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_ROUTE;

import datadog.trace.api.TagMap;
import datadog.trace.api.TagStore;
import datadog.trace.api.endpoint.EndpointResolver;
import datadog.trace.api.internal.VisibleForTesting;
import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks;
Expand All @@ -30,7 +30,7 @@
* <li>If {@code alwaysSimplifiedEndpoint=true}, always compute from URL regardless of route
* </ul>
*/
public class HttpEndpointPostProcessor extends TagsPostProcessor {
public class HttpEndpointPostProcessor extends EagerTagsPostProcessor {
private static final Logger log = LoggerFactory.getLogger(HttpEndpointPostProcessor.class);

private final EndpointResolver endpointResolver;
Expand All @@ -55,18 +55,18 @@ public HttpEndpointPostProcessor() {

@Override
public void processTags(
TagMap unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) {
TagStore unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) {
if (!endpointResolver.isEnabled()) {
log.debug("EndpointResolver is not enabled, skipping HTTP endpoint post processing");
return;
}

if (unsafeTags.getObject(HTTP_METHOD) == null) {
if (unsafeTags.load(HTTP_METHOD_ID) == null) {
return;
}

try {
String httpRoute = unsafeTags.getString(HTTP_ROUTE);
String httpRoute = unsafeTags.loadString(HTTP_ROUTE_ID);
Object httpUrl = urlOrPath(unsafeTags);
endpointResolver.resolveEndpoint(
unsafeTags, httpRoute, httpUrl == null ? null : httpUrl.toString());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package datadog.trace.core.tagprocessor;

import static datadog.trace.bootstrap.instrumentation.api.Tags.VERSION;
import static datadog.trace.api.KnownTags.DD_BASE_SERVICE_ID;
import static datadog.trace.api.KnownTags.VERSION_ID;

import datadog.trace.api.DDTags;
import datadog.trace.api.TagMap;
import datadog.trace.api.TagStore;
import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks;
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
import datadog.trace.core.DDSpanContext;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

public final class InternalTagsAdder extends TagsPostProcessor {
public final class InternalTagsAdder extends EagerTagsPostProcessor {
private final UTF8BytesString ddService;

// Prebuilt once to avoid per-span Entry allocation.
Expand All @@ -19,30 +20,30 @@ public final class InternalTagsAdder extends TagsPostProcessor {

public InternalTagsAdder(@Nonnull final String ddService, @Nullable final String version) {
this.ddService = UTF8BytesString.create(ddService);
this.baseServiceEntry = TagMap.Entry.create(DDTags.BASE_SERVICE, this.ddService);
this.baseServiceEntry = TagMap.Entry.create(DD_BASE_SERVICE_ID, this.ddService);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once dense store is on by default, we can drop the TagMap.Entry creation in the post processors.

this.versionEntry =
version != null && !version.isEmpty()
? TagMap.Entry.create(VERSION, UTF8BytesString.create(version))
? TagMap.Entry.create(VERSION_ID, UTF8BytesString.create(version))
: null;
}

@Override
public void processTags(
TagMap unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) {
TagStore unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) {
if (spanContext == null) {
return;
}

if (!ddService.toString().equalsIgnoreCase(spanContext.getServiceName())) {
if (baseServiceEntry != null) {
// service name != DD_SERVICE
unsafeTags.set(baseServiceEntry);
unsafeTags.store(baseServiceEntry);
}
} else {
// as per config consistency, the version tag is added across tracers only if
// the service name is DD_SERVICE and version tag is not manually set
if (versionEntry != null && !unsafeTags.containsKey(VERSION)) {
unsafeTags.set(versionEntry);
if (versionEntry != null && unsafeTags.load(VERSION_ID) == null) {
unsafeTags.store(versionEntry);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
package datadog.trace.core.tagprocessor;

import static datadog.trace.api.KnownTags.DD_PEER_SERVICE_REMAPPED_FROM_ID;
import static datadog.trace.api.KnownTags.PEER_SERVICE_ID;

import datadog.trace.api.Config;
import datadog.trace.api.DDTags;
import datadog.trace.api.TagMap;
import datadog.trace.api.TagStore;
import datadog.trace.api.internal.VisibleForTesting;
import datadog.trace.api.naming.NamingSchema;
import datadog.trace.api.naming.SpanNaming;
import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks;
import datadog.trace.bootstrap.instrumentation.api.Tags;
import datadog.trace.core.DDSpanContext;
import java.util.Map;
import javax.annotation.Nonnull;

public final class PeerServiceCalculator extends TagsPostProcessor {
public final class PeerServiceCalculator extends EagerTagsPostProcessor {
private final NamingSchema.ForPeerService peerServiceNaming;

private final Map<String, String> peerServiceMapping;
Expand All @@ -34,8 +35,8 @@ public PeerServiceCalculator() {

@Override
public void processTags(
TagMap unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) {
Object peerService = unsafeTags.getObject(Tags.PEER_SERVICE);
TagStore unsafeTags, DDSpanContext spanContext, AppendableSpanLinks spanLinks) {
Object peerService = unsafeTags.load(PEER_SERVICE_ID);
// the user set it
if (peerService != null) {
if (canRemap) {
Expand All @@ -46,18 +47,18 @@ public void processTags(
// calculate the defaults (if any)
peerServiceNaming.tags(unsafeTags);
// only remap if the mapping is not empty (saves one get)
remapPeerService(unsafeTags, canRemap ? unsafeTags.getObject(Tags.PEER_SERVICE) : null);
remapPeerService(unsafeTags, canRemap ? unsafeTags.load(PEER_SERVICE_ID) : null);
return;
}
// we have no peer.service and we do not compute defaults. Leave the map untouched
}

private void remapPeerService(TagMap unsafeTags, Object value) {
private void remapPeerService(TagStore unsafeTags, Object value) {
if (value != null) {
String mapped = peerServiceMapping.get(value);
if (mapped != null) {
unsafeTags.put(Tags.PEER_SERVICE, mapped);
unsafeTags.put(DDTags.PEER_SERVICE_REMAPPED_FROM, value);
unsafeTags.store(PEER_SERVICE_ID, mapped);
unsafeTags.store(DD_PEER_SERVICE_REMAPPED_FROM_ID, value);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ public final class TagsPostProcessorFactory {
private static boolean addRemoteHostname = true;

private static class Lazy {
private static TagsPostProcessor eagerProcessor = createEagerChain();
private static EagerTagsPostProcessor eagerProcessor = createEagerChain();
private static TagsPostProcessor lazyProcessor = createLazyChain();

private static TagsPostProcessor createEagerChain() {
final List<TagsPostProcessor> processors = new ArrayList<>(3);
private static EagerTagsPostProcessor createEagerChain() {
final List<EagerTagsPostProcessor> processors = new ArrayList<>(3);
processors.add(new PeerServiceCalculator());
if (addInternalTags) {
processors.add(
Expand All @@ -24,7 +24,7 @@ private static TagsPostProcessor createEagerChain() {
if (Config.get().isTraceResourceRenamingEnabled()) {
processors.add(new HttpEndpointPostProcessor());
}
return new PostProcessorChain(processors.toArray(new TagsPostProcessor[0]));
return new EagerPostProcessorChain(processors.toArray(new EagerTagsPostProcessor[0]));
}

private static TagsPostProcessor createLazyChain() {
Expand Down Expand Up @@ -54,7 +54,7 @@ private static TagsPostProcessor createLazyChain() {
}
}

public static TagsPostProcessor eagerProcessor() {
public static EagerTagsPostProcessor eagerProcessor() {
return Lazy.eagerProcessor;
}

Expand Down
Loading
Loading