diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/TracerInstaller.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/TracerInstaller.java index 27ef91c423b..545763b693d 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/TracerInstaller.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/TracerInstaller.java @@ -1,11 +1,17 @@ package datadog.trace.agent.tooling; import datadog.communication.ddagent.SharedCommunicationObjects; +import datadog.environment.OperatingSystem; 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.CoreTracer.CoreTracerBuilder; +import datadog.trace.core.servicediscovery.ForeignMemoryWriter; +import datadog.trace.core.servicediscovery.ServiceDiscovery; +import de.thetaphi.forbiddenapis.SuppressForbidden; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,14 +23,16 @@ public static synchronized void installGlobalTracer( ProfilingContextIntegration profilingContextIntegration) { if (Config.get().isTraceEnabled() || Config.get().isCiVisibilityEnabled()) { if (!(GlobalTracer.get() instanceof CoreTracer)) { - CoreTracer tracer = + CoreTracerBuilder tracerBuilder = CoreTracer.builder() .sharedCommunicationObjects(sharedCommunicationObjects) .profilingContextIntegration(profilingContextIntegration) .reportInTracerFlare() - .pollForTracingConfiguration() - .build(); - installGlobalTracer(tracer); + .pollForTracingConfiguration(); + + maybeEnableServiceDiscovery(tracerBuilder); + + installGlobalTracer(tracerBuilder.build()); } else { log.debug("GlobalTracer already registered."); } @@ -33,6 +41,35 @@ public static synchronized void installGlobalTracer( } } + @SuppressForbidden // intentional use of Class.forName + private static void maybeEnableServiceDiscovery(CoreTracerBuilder tracerBuilder) { + if (!OperatingSystem.isLinux()) { + log.debug("service discovery not supported outside linux"); + return; + } + // make sure this branch is not considered possible for graalvm artifact + if (Platform.isNativeImageBuilder() || Platform.isNativeImage()) { + log.debug("service discovery not supported on native images"); + return; + } + tracerBuilder.serviceDiscoveryFactory( + () -> { + try { + // use reflection to load MemFDUnixWriter so it doesn't get picked up when we + // transitively look for all tracer class dependencies to install in GraalVM via + // VMRuntimeInstrumentation + Class memFdClass = + Class.forName("datadog.trace.agent.tooling.servicediscovery.MemFDUnixWriter"); + ForeignMemoryWriter memFd = + (ForeignMemoryWriter) memFdClass.getConstructor().newInstance(); + return new ServiceDiscovery(memFd); + } catch (Throwable e) { + log.debug("service discovery not supported", e); + return null; + } + }); + } + public static void installGlobalTracer(final CoreTracer tracer) { try { GlobalTracer.registerIfAbsent(tracer); diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/servicediscovery/MemFDUnixWriter.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/servicediscovery/MemFDUnixWriter.java new file mode 100644 index 00000000000..3647c1f19cb --- /dev/null +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/servicediscovery/MemFDUnixWriter.java @@ -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. + } +} diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index 5184a72d6da..18112a26458 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -96,6 +96,8 @@ 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.servicediscovery.ServiceDiscoveryFactory; import datadog.trace.core.taginterceptor.RuleFlags; import datadog.trace.core.taginterceptor.TagInterceptor; import datadog.trace.core.traceinterceptor.LatencyTraceInterceptor; @@ -321,6 +323,7 @@ public static class CoreTracerBuilder { private TagInterceptor tagInterceptor; private boolean strictTraceWrites; private InstrumentationGateway instrumentationGateway; + private ServiceDiscoveryFactory serviceDiscoveryFactory; private TimeSource timeSource; private DataStreamsMonitoring dataStreamsMonitoring; private ProfilingContextIntegration profilingContextIntegration = @@ -436,6 +439,12 @@ public CoreTracerBuilder instrumentationGateway(InstrumentationGateway instrumen return this; } + public CoreTracerBuilder serviceDiscoveryFactory( + ServiceDiscoveryFactory serviceDiscoveryFactory) { + this.serviceDiscoveryFactory = serviceDiscoveryFactory; + return this; + } + public CoreTracerBuilder timeSource(TimeSource timeSource) { this.timeSource = timeSource; return this; @@ -528,6 +537,7 @@ public CoreTracer build() { tagInterceptor, strictTraceWrites, instrumentationGateway, + serviceDiscoveryFactory, timeSource, dataStreamsMonitoring, profilingContextIntegration, @@ -588,6 +598,7 @@ private CoreTracer( tagInterceptor, strictTraceWrites, instrumentationGateway, + null, // you might refactor this as well timeSource, dataStreamsMonitoring, profilingContextIntegration, @@ -619,6 +630,7 @@ private CoreTracer( final TagInterceptor tagInterceptor, final boolean strictTraceWrites, final InstrumentationGateway instrumentationGateway, + final ServiceDiscoveryFactory serviceDiscoveryFactory, final TimeSource timeSource, final DataStreamsMonitoring dataStreamsMonitoring, final ProfilingContextIntegration profilingContextIntegration, @@ -887,6 +899,22 @@ private CoreTracer( this.localRootSpanTagsNeedIntercept = this.tagInterceptor.needsIntercept(this.localRootSpanTags); + if (serviceDiscoveryFactory != null) { + AgentTaskScheduler.get() + .schedule( + () -> { + final ServiceDiscovery serviceDiscovery = + serviceDiscoveryFactory.createServiceDiscovery(); + if (serviceDiscovery != null) { + // JNA can do ldconfig and other commands. Those are hidden since internal. + try (final TraceScope blackhole = muteTracing()) { + serviceDiscovery.writeTracerMetadata(config); + } + } + }, + 1, + SECONDS); + } } /** Used by AgentTestRunner to inject configuration into the test tracer. */ diff --git a/dd-trace-core/src/main/java/datadog/trace/core/servicediscovery/ForeignMemoryWriter.java b/dd-trace-core/src/main/java/datadog/trace/core/servicediscovery/ForeignMemoryWriter.java new file mode 100644 index 00000000000..f84e19de297 --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/core/servicediscovery/ForeignMemoryWriter.java @@ -0,0 +1,5 @@ +package datadog.trace.core.servicediscovery; + +public interface ForeignMemoryWriter { + void write(byte[] payload); +} diff --git a/dd-trace-core/src/main/java/datadog/trace/core/servicediscovery/ServiceDiscovery.java b/dd-trace-core/src/main/java/datadog/trace/core/servicediscovery/ServiceDiscovery.java new file mode 100644 index 00000000000..14093987837 --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/core/servicediscovery/ServiceDiscovery.java @@ -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); + 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(), + 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); + + 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; + } +} diff --git a/dd-trace-core/src/main/java/datadog/trace/core/servicediscovery/ServiceDiscoveryFactory.java b/dd-trace-core/src/main/java/datadog/trace/core/servicediscovery/ServiceDiscoveryFactory.java new file mode 100644 index 00000000000..bb5884bd9b1 --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/core/servicediscovery/ServiceDiscoveryFactory.java @@ -0,0 +1,6 @@ +package datadog.trace.core.servicediscovery; + +@FunctionalInterface +public interface ServiceDiscoveryFactory { + ServiceDiscovery createServiceDiscovery(); +} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/servicediscovery/ServiceDiscoveryTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/servicediscovery/ServiceDiscoveryTest.groovy new file mode 100644 index 00000000000..a25d4b2641a --- /dev/null +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/servicediscovery/ServiceDiscoveryTest.groovy @@ -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"}' + } +}