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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 44 additions & 7 deletions dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
* <p>Spans are created by the {@link CoreTracer#buildSpan}. This implementation adds some features
* according to the DD agent.
*/
public class DDSpan implements AgentSpan, CoreSpan<DDSpan>, AttachableWrapper {
public class DDSpan implements AgentSpan, CoreSpan<DDSpan>, SpanLinkAccessor, AttachableWrapper {
private static final Logger log = LoggerFactory.getLogger(DDSpan.class);

static DDSpan create(
Expand Down Expand Up @@ -116,7 +116,8 @@ static DDSpan create(
*/
private volatile int longRunningVersion = 0;

protected final List<AgentSpanLink> links;
private static final List<AgentSpanLink> EMPTY = Collections.emptyList();
Copy link
Copy Markdown
Contributor

@amarziali amarziali Mar 12, 2026

Choose a reason for hiding this comment

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

There is an issue with SpanPointersProcessor. In fact TagsPostProcessors manipulate links directly and adding something to an emptyList will cause an UnsupportedOperationException. What can be done, is to use an empty non threadsafe list when calling tag processors if the original is empty and eventually batch adding the list at the end if the processors added something

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Few ideas here:

  1. There is a TODO from Doug about the processor that mutates the span links:

// DQH - TODO - There's a lot room to optimize this using TagMap's capabilities

Does it mean it can skip span links and use tags directly?

  1. As processors only add span links, we can replace the span links collection by a method reference to the addSpanLink method as Consumer<SpanLink>. This will avoid exposing the span link implementation and we can keep it null until there is effectively span link stored.

Copy link
Copy Markdown
Contributor Author

@dougqh dougqh Mar 12, 2026

Choose a reason for hiding this comment

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

That's a good catch.
I do think we need to improve the encapsulation; otherwise, these changes are going to be hard to make.

As for my prior TODO, TagMap has a bunch of methods for removeAndGetEntry, getString, stringValue, etc that could be used in the various *spanPointer methods. In theory, they could simplify the code significantly.

Copy link
Copy Markdown
Contributor Author

@dougqh dougqh Mar 12, 2026

Choose a reason for hiding this comment

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

On a related note, I'm curious why spanLinks was put on DDSpan rather than DDSpanContext.
It seems like it would be more consistent to put on DDSpanContext and then we wouldn't have this issue.

I also think we might be pool DDSpanContext in the future, so I'd like to avoid adding more to DDSpan.

Copy link
Copy Markdown
Contributor

@PerfectSlayer PerfectSlayer Mar 13, 2026

Choose a reason for hiding this comment

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

I'm curious why spanLinks was put on DDSpan rather than DDSpanContext.

SpanContext was supposed to describe "a span context", not to hold the span data model.
We should be able to use SpanContext as reference descriptor for spans, but they end up holding way too much data.
So when I add to implement span links, I made sure to leave them in Span, not SpanContext.


I had a look at its Javadoc and it basically should only be IDs (trace + span), sampling, a bit of (W3C) trace state and baggage, nothing else :)

Copy link
Copy Markdown
Contributor Author

@dougqh dougqh Mar 24, 2026

Choose a reason for hiding this comment

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

Okay, that makes sense. I think that maybe we should repurpose SpanContext into SpanContents.

I have a bit of crazy idea to recycle SpanContents, but I want to do so while maintaining Span object identity. In other words, I'd always create a new Span, but the SpanContents inside the span would be reused.

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.

I've addressed the TagPostProcessor interaction by introducing two new interfaces: WritableSpanLinks and SpanLinkAccessor.

WritableSpanLinks just provides the ability to addLink, it is passed to TagPostProcessor. SpanLinkAccessor extends WritableSpanLinks and also provides a way to get the links.

DDSpan now implements SpanLinkAccessor. I picked this design, so I could avoid a capturing lambda around the span.

protected volatile List<AgentSpanLink> links;

/**
* Spans should be constructed using the builder, not by calling the constructor directly.
Expand Down Expand Up @@ -144,7 +145,7 @@ private DDSpan(
context.getTraceCollector().touch(); // external clock: explicitly update lastReferenced
}

this.links = links == null ? new CopyOnWriteArrayList<>() : new CopyOnWriteArrayList<>(links);
this.links = links == null || links.isEmpty() ? EMPTY : new CopyOnWriteArrayList<>(links);
}

public boolean isFinished() {
Expand Down Expand Up @@ -758,12 +759,12 @@ public CharSequence getType() {

@Override
public void processServiceTags() {
context.earlyProcessTags(links);
context.earlyProcessTags(this);
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.

this DDSpan is now a SpanLinkAccessor / WritableSpanLinks

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.

Updated - now just an AppendableSpanLinks (previously WritableSpanLinks)
SpanLinkAccessor has been removed

}

@Override
public void processTagsAndBaggage(final MetadataConsumer consumer) {
context.processTagsAndBaggage(consumer, longRunningVersion, links);
context.processTagsAndBaggage(consumer, longRunningVersion, this);
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.

this DDSpan is now a SpanLinkAccessor / WritableSpanLinks

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.

Updated - now just an AppendableSpanLinks (previously WritableSpanLinks)
SpanLinkAccessor has been removed

}

@Override
Expand Down Expand Up @@ -876,11 +877,47 @@ public void setLongRunningVersion(int longRunningVersion) {
public TraceConfig traceConfig() {
return context.getTraceCollector().getTraceConfig();
}

@Override
public List<? extends AgentSpanLink> getLinks() {
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.

I don't entirely like that I had to add this public method, but I took care to make the signature ? extends AgentSpanLink to discourage writing to the list directly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same, I don't like exposing the links. It was a strong design constraints when I added span links.
The tags and baggage handling are already so messy, I don't want to expose the span links collections.

Additionally, the Javadoc for this method states the collection should be an immutable collection where the return collection is mutable. That's even worst if we want to prevent to 1. show the internal span links implementation 2. contributors to modify span links outside DDSpan.

I have open a Slack discussion about fixing the root design issue before trying to unlocking more public API.

Copy link
Copy Markdown
Contributor Author

@dougqh dougqh Mar 26, 2026

Choose a reason for hiding this comment

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

Yes, I didn't add an immutable wrapper that would negate the some of the gains from this change.
The reason that I called it immutable is because the specific type of ? extends AgentSpanLink prevents calling most of the modification methods without casting.

For now, I think the best option might be to just drop the SpanLinkAccessor interface and pass DDSpan directly to DDSpanContext. I was trying to restrict what DDSpanContext touches during processing, but the trade-off of adding a public getLinks might not be worth it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For now, I think the best option might be to just drop the SpanLinkAccessor interface and pass DDSpan directly to DDSpanContext. I was trying to restrict what DDSpanContext touches during processing, but the trade-off of adding a public getLinks might not be worth it.

That would indeed "solve" the point I raised here #10822 (comment)

return this.links;
}

@Override
public void addLink(AgentSpanLink link) {
if (link != null) {
this.links.add(link);
if (link == null) {
return;
}

// If links are initially null / empty, then the shared placeholder List EMPTY is used.
// Bacause EMPTY is shared, EMPTY is safe for reading, but not for writing.
// On write - if links is the EMPTY placeholder, then need to create a CopyOnWriteArrayList
// owned by this DDSpan

// Creation of the CopyOnWriteArrayList is done via double checking locking using volatile &
// synchronized

// If before or inside the synchronized block, links no longer points to EMPTY,
// then this thread or another thread has already handled the list construction,
// so just add to the list

// If links still points to EMPTY inside the synchronized block, then construct a new
// CopyOnWriteArrayList
// containing the newly added link

List<AgentSpanLink> links = this.links;
if (links != EMPTY) {
links.add(link);
return;
}

synchronized (this) {
links = this.links;
if (links != EMPTY) {
links.add(link);
} else {
this.links = new CopyOnWriteArrayList<>(Collections.singletonList(link));
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities;
import datadog.trace.bootstrap.instrumentation.api.Tags;
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
import datadog.trace.bootstrap.instrumentation.api.WritableSpanLinks;
import datadog.trace.core.propagation.PropagationTags;
import datadog.trace.core.taginterceptor.TagInterceptor;
import datadog.trace.core.tagprocessor.TagsPostProcessorFactory;
Expand Down Expand Up @@ -1087,19 +1088,19 @@ public <T> void setMetaStruct(final String field, final T value) {
}
}

public void earlyProcessTags(List<AgentSpanLink> links) {
public void earlyProcessTags(SpanLinkAccessor links) {
synchronized (unsafeTags) {
TagsPostProcessorFactory.eagerProcessor().processTags(unsafeTags, this, links);
}
}

public void processTagsAndBaggage(
final MetadataConsumer consumer, int longRunningVersion, List<AgentSpanLink> links) {
final MetadataConsumer consumer, int longRunningVersion, SpanLinkAccessor links) {
synchronized (unsafeTags) {
// Tags
TagsPostProcessorFactory.lazyProcessor().processTags(unsafeTags, this, links);

String linksTag = DDSpanLink.toTag(links);
String linksTag = DDSpanLink.toTag(links.getLinks());
if (linksTag != null) {
unsafeTags.put(SPAN_LINKS, linksTag);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public static SpanLink from(ExtractedContext context, SpanAttributes attributes)
* @param links The span link collection to encode.
* @return The encoded tag value, {@code null} if no links.
*/
public static String toTag(List<AgentSpanLink> links) {
public static String toTag(List<? extends AgentSpanLink> links) {
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.

Updated to accommodate return from SpanLinkAccessor.getLinks

if (links == null || links.isEmpty()) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package datadog.trace.core;

import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink;
import datadog.trace.bootstrap.instrumentation.api.WritableSpanLinks;
import java.util.List;

public interface SpanLinkAccessor extends WritableSpanLinks {
Comment thread
dougqh marked this conversation as resolved.
Outdated
public List<? extends AgentSpanLink> getLinks();
Comment thread
dougqh marked this conversation as resolved.
Outdated
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import datadog.trace.api.TagMap;
import datadog.trace.api.endpoint.EndpointResolver;
import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink;
import datadog.trace.bootstrap.instrumentation.api.WritableSpanLinks;
import datadog.trace.core.DDSpanContext;
import java.util.List;
import org.slf4j.Logger;
Expand Down Expand Up @@ -56,7 +57,7 @@ public HttpEndpointPostProcessor() {

@Override
public void processTags(
TagMap unsafeTags, DDSpanContext spanContext, List<AgentSpanLink> spanLinks) {
TagMap unsafeTags, DDSpanContext spanContext, WritableSpanLinks spanLinks) {
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.

I introduced WritableSpanLinks as way to update the span links inside of TagPostProcessors.

The reason for the new interface is that I wanted to avoid creating a capturing lambda around the DDSpan, so instead I have DDSpan implement WritableSpanLinks.

if (!endpointResolver.isEnabled()) {
log.debug("EndpointResolver is not enabled, skipping HTTP endpoint post processing");
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
import static datadog.trace.api.DDTags.DD_INTEGRATION;

import datadog.trace.api.TagMap;
import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink;
import datadog.trace.bootstrap.instrumentation.api.WritableSpanLinks;
import datadog.trace.core.DDSpanContext;
import java.util.List;

public class IntegrationAdder extends TagsPostProcessor {
@Override
public void processTags(
TagMap unsafeTags, DDSpanContext spanContext, List<AgentSpanLink> spanLinks) {
TagMap unsafeTags, DDSpanContext spanContext, WritableSpanLinks spanLinks) {
final CharSequence instrumentationName = spanContext.getIntegrationName();
if (instrumentationName != null) {
unsafeTags.set(DD_INTEGRATION, instrumentationName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import datadog.trace.api.DDTags;
import datadog.trace.api.TagMap;
import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink;
import datadog.trace.bootstrap.instrumentation.api.WritableSpanLinks;
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
import datadog.trace.core.DDSpanContext;
import java.util.List;
Expand All @@ -21,7 +21,7 @@ public InternalTagsAdder(@Nullable final String ddService, @Nullable final Strin

@Override
public void processTags(
TagMap unsafeTags, DDSpanContext spanContext, List<AgentSpanLink> spanLinks) {
TagMap unsafeTags, DDSpanContext spanContext, WritableSpanLinks spanLinks) {
if (spanContext == null || ddService == null) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import datadog.trace.api.TagMap;
import datadog.trace.api.telemetry.LogCollector;
import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink;
import datadog.trace.bootstrap.instrumentation.api.WritableSpanLinks;
import datadog.trace.core.DDSpanContext;
import datadog.trace.core.util.JsonStreamParser;
import datadog.trace.payloadtags.PayloadTagsData;
Expand Down Expand Up @@ -72,7 +73,7 @@ public static PayloadTagsProcessor create(Config config) {

@Override
public void processTags(
TagMap unsafeTags, DDSpanContext spanContext, List<AgentSpanLink> spanLinks) {
TagMap unsafeTags, DDSpanContext spanContext, WritableSpanLinks spanLinks) {
int spanMaxTags = maxTags + unsafeTags.size();
for (Map.Entry<String, RedactionRules> tagPrefixRedactionRules :
redactionRulesByTagPrefix.entrySet()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import datadog.trace.api.TagMap;
import datadog.trace.api.naming.NamingSchema;
import datadog.trace.api.naming.SpanNaming;
import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink;
import datadog.trace.bootstrap.instrumentation.api.WritableSpanLinks;
import datadog.trace.bootstrap.instrumentation.api.Tags;
import datadog.trace.core.DDSpanContext;
import java.util.List;
Expand Down Expand Up @@ -34,7 +34,7 @@ public PeerServiceCalculator() {

@Override
public void processTags(
TagMap unsafeTags, DDSpanContext spanContext, List<AgentSpanLink> spanLinks) {
TagMap unsafeTags, DDSpanContext spanContext, WritableSpanLinks spanLinks) {
Object peerService = unsafeTags.getObject(Tags.PEER_SERVICE);
// the user set it
if (peerService != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package datadog.trace.core.tagprocessor;

import datadog.trace.api.TagMap;
import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink;
import datadog.trace.bootstrap.instrumentation.api.WritableSpanLinks;
import datadog.trace.core.DDSpanContext;
import java.util.List;
import java.util.Objects;
Expand All @@ -16,7 +16,7 @@ public PostProcessorChain(@Nonnull final TagsPostProcessor... processors) {

@Override
public void processTags(
TagMap unsafeTags, DDSpanContext spanContext, List<AgentSpanLink> spanLinks) {
TagMap unsafeTags, DDSpanContext spanContext, WritableSpanLinks spanLinks) {
for (final TagsPostProcessor tagsPostProcessor : chain) {
tagsPostProcessor.processTags(unsafeTags, spanContext, spanLinks);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import com.google.re2j.PatternSyntaxException;
import datadog.trace.api.DDTags;
import datadog.trace.api.TagMap;
import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink;
import datadog.trace.bootstrap.instrumentation.api.WritableSpanLinks;
import datadog.trace.bootstrap.instrumentation.api.Tags;
import datadog.trace.core.DDSpanContext;
import datadog.trace.util.Strings;
Expand Down Expand Up @@ -59,7 +59,7 @@ private String obfuscate(String query) {

@Override
public void processTags(
TagMap unsafeTags, DDSpanContext spanContext, List<AgentSpanLink> spanLinks) {
TagMap unsafeTags, DDSpanContext spanContext, WritableSpanLinks spanLinks) {
Object query = unsafeTags.getObject(DDTags.HTTP_QUERY);
if (query instanceof CharSequence) {
query = obfuscate(query.toString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

import datadog.trace.api.DDTags;
import datadog.trace.api.TagMap;
import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink;
import datadog.trace.bootstrap.instrumentation.api.WritableSpanLinks;
import datadog.trace.core.DDSpanContext;
import java.util.List;
import java.util.function.Supplier;

public final class RemoteHostnameAdder extends TagsPostProcessor {
Expand All @@ -16,7 +15,7 @@ public RemoteHostnameAdder(Supplier<String> hostnameSupplier) {

@Override
public void processTags(
TagMap unsafeTags, DDSpanContext spanContext, List<AgentSpanLink> spanLinks) {
TagMap unsafeTags, DDSpanContext spanContext, WritableSpanLinks spanLinks) {
if (spanContext.getSpanId() == spanContext.getRootSpanId()) {
unsafeTags.put(DDTags.TRACER_HOST, hostnameSupplier.get());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
import static datadog.trace.api.DDTags.DD_SVC_SRC;

import datadog.trace.api.TagMap;
import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink;
import datadog.trace.bootstrap.instrumentation.api.WritableSpanLinks;
import datadog.trace.core.DDSpanContext;
import java.util.List;

public class ServiceNameSourceAdder extends TagsPostProcessor {
@Override
public void processTags(
TagMap unsafeTags, DDSpanContext spanContext, List<AgentSpanLink> spanLinks) {
TagMap unsafeTags, DDSpanContext spanContext, WritableSpanLinks spanLinks) {
final CharSequence serviceNameSource = spanContext.getServiceNameSource();
if (serviceNameSource != null) {
unsafeTags.set(DD_SVC_SRC, serviceNameSource);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink;
import datadog.trace.bootstrap.instrumentation.api.SpanAttributes;
import datadog.trace.bootstrap.instrumentation.api.SpanLink;
import datadog.trace.bootstrap.instrumentation.api.WritableSpanLinks;
import datadog.trace.core.DDSpanContext;
import datadog.trace.util.Strings;
import java.nio.charset.StandardCharsets;
Expand All @@ -38,16 +39,16 @@ public class SpanPointersProcessor extends TagsPostProcessor {

@Override
public void processTags(
TagMap unsafeTags, DDSpanContext spanContext, List<AgentSpanLink> spanLinks) {
TagMap unsafeTags, DDSpanContext spanContext, WritableSpanLinks spanLinks) {
// DQH - TODO - There's a lot room to optimize this using TagMap's capabilities
AgentSpanLink s3Link = handleS3SpanPointer(unsafeTags);
if (s3Link != null) {
spanLinks.add(s3Link);
spanLinks.addLink(s3Link);
}

AgentSpanLink dynamoDbLink = handleDynamoDbSpanPointer(unsafeTags);
if (dynamoDbLink != null) {
spanLinks.add(dynamoDbLink);
spanLinks.addLink(dynamoDbLink);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import datadog.trace.api.TagMap;
import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink;
import datadog.trace.bootstrap.instrumentation.api.WritableSpanLinks;
import datadog.trace.core.DDSpanContext;
import java.util.List;
import java.util.Map;
Expand All @@ -12,12 +13,12 @@ public abstract class TagsPostProcessor {
*/
@Deprecated
final Map<String, Object> processTags(
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.

Before I merge this PR, I'm going to get rid of this vestige of the old signature. I just have to update a bunch of tests, but I'm already touching a bunch of those files anyway.

Map<String, Object> unsafeTags, DDSpanContext context, List<AgentSpanLink> links) {
Map<String, Object> unsafeTags, DDSpanContext context, List<AgentSpanLink> spanLinks) {
TagMap map = TagMap.fromMap(unsafeTags);
this.processTags(map, context, links);
this.processTags(map, context, spanLinks::add);
return map;
}

public abstract void processTags(
TagMap unsafeTags, DDSpanContext spanContext, List<AgentSpanLink> spanLinks);
TagMap unsafeTags, DDSpanContext spanContext, WritableSpanLinks spanLinks);
}
Loading