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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package datadog.trace.agent.tooling;

import datadog.communication.ddagent.SharedCommunicationObjects;
import datadog.environment.OperatingSystem;
import datadog.trace.agent.tooling.servicediscovery.MemFDUnixWriter;
import datadog.trace.api.Config;
import datadog.trace.api.GlobalTracer;
import datadog.trace.api.Platform;
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
import datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration;
import datadog.trace.core.CoreTracer;
import datadog.trace.core.servicediscovery.ServiceDiscovery;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -23,6 +27,7 @@ public static synchronized void installGlobalTracer(
.profilingContextIntegration(profilingContextIntegration)
.reportInTracerFlare()
.pollForTracingConfiguration()
.serviceDiscovery(getServiceDiscovery())
.build();
installGlobalTracer(tracer);
} else {
Expand All @@ -33,6 +38,19 @@ public static synchronized void installGlobalTracer(
}
}

private static ServiceDiscovery getServiceDiscovery() {
if (!OperatingSystem.isLinux()) {
log.debug("service discovery not supported outside linux");
return null;
}
// make sure this branch is not considered possible for graalvm artifact
if (!Platform.isNativeImageBuilder() && !Platform.isNativeImage()) {
return new ServiceDiscovery(new MemFDUnixWriter());
}
log.debug("service discovery not supported on native images");
return null;
}

public static void installGlobalTracer(final CoreTracer tracer) {
try {
GlobalTracer.registerIfAbsent(tracer);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package datadog.trace.agent.tooling.servicediscovery;

import com.sun.jna.Library;
import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.NativeLong;
import com.sun.jna.Pointer;
import datadog.trace.core.servicediscovery.ForeignMemoryWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MemFDUnixWriter implements ForeignMemoryWriter {
private static final Logger log = LoggerFactory.getLogger(MemFDUnixWriter.class);

private interface LibC extends Library {
int memfd_create(String name, int flags);

NativeLong write(int fd, Pointer buf, NativeLong count);

int fcntl(int fd, int cmd, int arg);
}

// https://elixir.bootlin.com/linux/v6.17.1/source/include/uapi/linux/memfd.h#L8-L9
private static final int MFD_CLOEXEC = 0x0001;
private static final int MFD_ALLOW_SEALING = 0x0002;

// https://elixir.bootlin.com/linux/v6.17.1/source/include/uapi/linux/fcntl.h#L40
private static final int F_ADD_SEALS = 1033; //

// https://elixir.bootlin.com/linux/v6.17.1/source/include/uapi/linux/fcntl.h#L46-L49
private static final int F_SEAL_SEAL = 0x0001;
private static final int F_SEAL_SHRINK = 0x0002;
private static final int F_SEAL_GROW = 0x0004;

@Override
public void write(byte[] payload) {
final LibC libc = Native.load("c", LibC.class);

int memFd = libc.memfd_create("datadog-tracer-info", MFD_CLOEXEC | MFD_ALLOW_SEALING);
if (memFd < 0) {
log.warn("memfd_create failed, errno={}", Native.getLastError());
return;
}

log.debug("datadog-tracer-info memfd created (fd={})", memFd);

Memory buf = new Memory(payload.length);
buf.write(0, payload, 0, payload.length);

NativeLong written = libc.write(memFd, buf, new NativeLong(payload.length));
if (written.longValue() != payload.length) {
log.warn("write to memfd failed errno={}", Native.getLastError());
return;
}
log.debug("wrote {} bytes to memfd {}", written.longValue(), memFd);
int returnCode = libc.fcntl(memFd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_SEAL);
if (returnCode == -1) {
log.warn("failed to add seal to memfd errno={}", Native.getLastError());
return;
}
// memfd is not closed to keep it readable for the lifetime of the process.
}
}
13 changes: 13 additions & 0 deletions dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
import datadog.trace.core.propagation.TracingPropagator;
import datadog.trace.core.propagation.XRayPropagator;
import datadog.trace.core.scopemanager.ContinuableScopeManager;
import datadog.trace.core.servicediscovery.ServiceDiscovery;
import datadog.trace.core.taginterceptor.RuleFlags;
import datadog.trace.core.taginterceptor.TagInterceptor;
import datadog.trace.core.traceinterceptor.LatencyTraceInterceptor;
Expand Down Expand Up @@ -321,6 +322,7 @@ public static class CoreTracerBuilder {
private TagInterceptor tagInterceptor;
private boolean strictTraceWrites;
private InstrumentationGateway instrumentationGateway;
private ServiceDiscovery serviceDiscovery;
private TimeSource timeSource;
private DataStreamsMonitoring dataStreamsMonitoring;
private ProfilingContextIntegration profilingContextIntegration =
Expand Down Expand Up @@ -436,6 +438,11 @@ public CoreTracerBuilder instrumentationGateway(InstrumentationGateway instrumen
return this;
}

public CoreTracerBuilder serviceDiscovery(ServiceDiscovery serviceDiscovery) {
this.serviceDiscovery = serviceDiscovery;
return this;
}

public CoreTracerBuilder timeSource(TimeSource timeSource) {
this.timeSource = timeSource;
return this;
Expand Down Expand Up @@ -528,6 +535,7 @@ public CoreTracer build() {
tagInterceptor,
strictTraceWrites,
instrumentationGateway,
serviceDiscovery,
timeSource,
dataStreamsMonitoring,
profilingContextIntegration,
Expand Down Expand Up @@ -588,6 +596,7 @@ private CoreTracer(
tagInterceptor,
strictTraceWrites,
instrumentationGateway,
null, // you might refactor this as well
timeSource,
dataStreamsMonitoring,
profilingContextIntegration,
Expand Down Expand Up @@ -619,6 +628,7 @@ private CoreTracer(
final TagInterceptor tagInterceptor,
final boolean strictTraceWrites,
final InstrumentationGateway instrumentationGateway,
final ServiceDiscovery serviceDiscovery,
final TimeSource timeSource,
final DataStreamsMonitoring dataStreamsMonitoring,
final ProfilingContextIntegration profilingContextIntegration,
Expand Down Expand Up @@ -887,6 +897,9 @@ private CoreTracer(

this.localRootSpanTagsNeedIntercept =
this.tagInterceptor.needsIntercept(this.localRootSpanTags);
if (serviceDiscovery != null) {
serviceDiscovery.writeTracerMetadata(config);
}
}

/** Used by AgentTestRunner to inject configuration into the test tracer. */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package datadog.trace.core.servicediscovery;

public interface ForeignMemoryWriter {
void write(byte[] payload);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package datadog.trace.core.servicediscovery;

import static java.nio.charset.StandardCharsets.ISO_8859_1;

import datadog.common.container.ContainerInfo;
import datadog.communication.ddagent.TracerVersion;
import datadog.communication.serialization.GrowableBuffer;
import datadog.communication.serialization.msgpack.MsgPackWriter;
import datadog.trace.api.Config;
import datadog.trace.api.ProcessTags;
import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString;
import datadog.trace.common.writer.ddagent.SimpleUtf8Cache;
import java.nio.ByteBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ServiceDiscovery {
private static final Logger log = LoggerFactory.getLogger(ServiceDiscovery.class);

private static final byte[] SCHEMA_VERSION = "schema_version".getBytes(ISO_8859_1);
Copy link
Contributor

@dougqh dougqh Oct 9, 2025

Choose a reason for hiding this comment

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

For this particular case, I'd rather not have the constants.
The problem is that they end up permanently consuming memory when we really only intend to use them once.

Admittedly, this is a weird case and it has got me thinking about whether we want to just unload the class, but that's on platform to figure out.

private static final byte[] RUNTIME_ID = "runtime_id".getBytes(ISO_8859_1);
private static final byte[] LANG = "tracer_language".getBytes(ISO_8859_1);
private static final byte[] TRACER_VERSION = "tracer_version".getBytes(ISO_8859_1);
private static final byte[] HOSTNAME = "hostname".getBytes(ISO_8859_1);
private static final byte[] SERVICE = "service_name".getBytes(ISO_8859_1);
private static final byte[] ENV = "service_env".getBytes(ISO_8859_1);
private static final byte[] SERVICE_VERSION = "service_version".getBytes(ISO_8859_1);
private static final byte[] PROCESS_TAGS = "process_tags".getBytes(ISO_8859_1);
private static final byte[] CONTAINER_ID = "container_id".getBytes(ISO_8859_1);
private static final byte[] JAVA_LANG = "java".getBytes(ISO_8859_1);

private final ForeignMemoryWriter foreignMemoryWriter;

public ServiceDiscovery(ForeignMemoryWriter foreignMemoryWriter) {
this.foreignMemoryWriter = foreignMemoryWriter;
}

public void writeTracerMetadata(Config config) {
byte[] payload =
ServiceDiscovery.encodePayload(
TracerVersion.TRACER_VERSION,
config.getHostName(),
config.getRuntimeId(),
config.getServiceName(),
Copy link
Contributor

Choose a reason for hiding this comment

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

This is using the statically configured service name which isn't necessarily the service name that the tracer will use in the end. Are we okay with that?

Copy link
Contributor

Choose a reason for hiding this comment

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

We might also want to consider moving this to a background task, so it doesn't impact start-up.

config.getEnv(),
config.getVersion(),
ProcessTags.getTagsForSerialization(),
ContainerInfo.get().getContainerId());

try {
foreignMemoryWriter.write(payload);
} catch (Throwable t) {
log.debug("service discovery memfd write failed", t);
}
}

static byte[] encodePayload(
String tracerVersion,
String hostname,
String runtimeID,
String service,
String env,
String serviceVersion,
UTF8BytesString processTags,
String containerID) {
GrowableBuffer buffer = new GrowableBuffer(1028);
MsgPackWriter writer = new MsgPackWriter(buffer);

int mapElements = 4;
mapElements += (runtimeID != null && !runtimeID.isEmpty()) ? 1 : 0;
mapElements += (service != null && !service.isEmpty()) ? 1 : 0;
mapElements += (env != null && !env.isEmpty()) ? 1 : 0;
mapElements += (serviceVersion != null && !serviceVersion.isEmpty()) ? 1 : 0;
mapElements += (processTags != null && processTags.length() > 0) ? 1 : 0;
mapElements += (containerID != null && !containerID.isEmpty()) ? 1 : 0;

SimpleUtf8Cache encodingCache = new SimpleUtf8Cache(256);
Copy link
Contributor

@dougqh dougqh Oct 9, 2025

Choose a reason for hiding this comment

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

I would skip the cache in this case.
Since the code is creating a new cache each time, the cache isn't providing a benefit here. And the cache is actually slower than just doing the encoding directly, the cache is helpful in rducing allocation if we're repeatedly encoding again and again.

But since this code is only called a handful of times, there's not much chance to save on allocation.


writer.startMap(mapElements);

writer.writeBinary(SCHEMA_VERSION);
writer.writeInt(2);

writer.writeBinary(LANG);
writer.writeBinary(JAVA_LANG);

writer.writeBinary(TRACER_VERSION);
writer.writeString(tracerVersion, encodingCache);

writer.writeBinary(HOSTNAME);
writer.writeString(hostname, encodingCache);

if (runtimeID != null && !runtimeID.isEmpty()) {
writer.writeBinary(RUNTIME_ID);
writer.writeString(runtimeID, encodingCache);
}
if (service != null && !service.isEmpty()) {
writer.writeBinary(SERVICE);
writer.writeString(service, encodingCache);
}
if (env != null && !env.isEmpty()) {
writer.writeBinary(ENV);
writer.writeString(env, encodingCache);
}
if (serviceVersion != null && !serviceVersion.isEmpty()) {
writer.writeBinary(SERVICE_VERSION);
writer.writeString(serviceVersion, encodingCache);
}
if (processTags != null && processTags.length() > 0) {
writer.writeBinary(PROCESS_TAGS);
writer.writeUTF8(processTags);
}
if (containerID != null && !containerID.isEmpty()) {
writer.writeBinary(CONTAINER_ID);
writer.writeString(containerID, encodingCache);
}

ByteBuffer byteBuffer = buffer.slice();
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.duplicate().get(bytes);
return bytes;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package datadog.trace.core.servicediscovery

import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString
import datadog.trace.core.test.DDCoreSpecification
import spock.lang.Timeout
import org.msgpack.core.MessagePack
import org.msgpack.value.MapValue


@Timeout(10)
class ServiceDiscoveryTest extends DDCoreSpecification {
def "encodePayload with all optional fields"() {
given:
String tracerVersion = "1.2.3"
String hostname = "test-host"
String runtimeID = "rid-123"
String service = "orders"
String env = "prod"
String serviceVersion = "1.1.1"
UTF8BytesString processTags = UTF8BytesString.create("key1:val1,key2:val2")
String containerID = "containerID"

when:
byte[] out = ServiceDiscovery.encodePayload(tracerVersion, hostname, runtimeID, service, env, serviceVersion, processTags, containerID)
MapValue map = MessagePack.newDefaultUnpacker(out).unpackValue().asMapValue()

then:
map.size() == 10
and:
map.toString() == '{"schema_version":2,"tracer_language":"java","tracer_version":"1.2.3","hostname":"test-host","runtime_id":"rid-123","service_name":"orders","service_env":"prod","service_version":"1.1.1","process_tags":"key1:val1,key2:val2","container_id":"containerID"}'
}

def "encodePayload only required fields"() {
given:
String tracerVersion = "1.2.3"
String hostname = "my_host"

when:
byte[] out = ServiceDiscovery.encodePayload(tracerVersion, hostname, null, null, null, null, null, null)
MapValue map = MessagePack.newDefaultUnpacker(out).unpackValue().asMapValue()

then:
map.size() == 4
and:
map.toString() == '{"schema_version":2,"tracer_language":"java","tracer_version":"1.2.3","hostname":"my_host"}'
}
}
Loading