diff --git a/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/NativeImageGeneratorRunnerInstrumentation.java b/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/NativeImageGeneratorRunnerInstrumentation.java index 1dc8679b989..3ebe7f59da4 100644 --- a/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/NativeImageGeneratorRunnerInstrumentation.java +++ b/dd-java-agent/instrumentation/graal/native-image/src/main/java/datadog/trace/instrumentation/graal/nativeimage/NativeImageGeneratorRunnerInstrumentation.java @@ -76,6 +76,7 @@ public static void onEnter(@Advice.Argument(value = 0, readOnly = false) String[ + "datadog.trace.api.Platform:rerun," + "datadog.trace.api.Platform$Captured:build_time," + "datadog.trace.api.env.CapturedEnvironment:build_time," + + "datadog.trace.api.env.CapturedEnvironment$ProcessInfo:build_time," + "datadog.trace.api.ConfigCollector:rerun," + "datadog.trace.api.ConfigDefaults:build_time," + "datadog.trace.api.ConfigOrigin:build_time," diff --git a/dd-java-agent/instrumentation/spring-boot/src/main/java/datadog/trace/instrumentation/springboot/SpringApplicationInstrumentation.java b/dd-java-agent/instrumentation/spring-boot/src/main/java/datadog/trace/instrumentation/springboot/SpringApplicationInstrumentation.java index dfddac80de1..215c10892e5 100644 --- a/dd-java-agent/instrumentation/spring-boot/src/main/java/datadog/trace/instrumentation/springboot/SpringApplicationInstrumentation.java +++ b/dd-java-agent/instrumentation/spring-boot/src/main/java/datadog/trace/instrumentation/springboot/SpringApplicationInstrumentation.java @@ -7,6 +7,7 @@ import datadog.trace.agent.tooling.Instrumenter; import datadog.trace.agent.tooling.InstrumenterModule; import datadog.trace.api.Config; +import datadog.trace.api.ProcessTags; import datadog.trace.bootstrap.instrumentation.api.AgentTracer; import net.bytebuddy.asm.Advice; import org.springframework.core.env.ConfigurableEnvironment; @@ -60,6 +61,18 @@ public static void afterEnvironmentPostProcessed( final String applicationName = environment.getProperty("spring.application.name"); if (applicationName != null && !applicationName.isEmpty()) { AgentTracer.get().updatePreferredServiceName(applicationName); + ProcessTags.addTag("springboot.application", applicationName); + } + if (Config.get().isExperimentalCollectProcessTagsEnabled()) { + final String[] profiles = environment.getActiveProfiles(); + if (profiles != null && profiles.length > 0) { + ProcessTags.addTag("springboot.profile", profiles[0]); + } else { + final String[] defaultProfiles = environment.getDefaultProfiles(); + if (defaultProfiles != null && defaultProfiles.length > 0) { + ProcessTags.addTag("springboot.profile", defaultProfiles[0]); + } + } } } } @@ -77,6 +90,18 @@ public static void afterEnvironmentPostProcessed( final String applicationName = environment.getProperty("spring.application.name"); if (applicationName != null && !applicationName.isEmpty()) { AgentTracer.get().updatePreferredServiceName(applicationName); + ProcessTags.addTag("springboot.application", applicationName); + } + if (Config.get().isExperimentalCollectProcessTagsEnabled()) { + final String[] profiles = environment.getActiveProfiles(); + if (profiles != null && profiles.length > 0) { + ProcessTags.addTag("springboot.profile", profiles[0]); + } else { + final String[] defaultProfiles = environment.getDefaultProfiles(); + if (defaultProfiles != null && defaultProfiles.length > 0) { + ProcessTags.addTag("springboot.profile", defaultProfiles[0]); + } + } } } } diff --git a/dd-java-agent/instrumentation/spring-boot/src/test/groovy/SpringBootApplicationTest.groovy b/dd-java-agent/instrumentation/spring-boot/src/test/groovy/SpringBootApplicationTest.groovy index 31a17c0b048..00694c25505 100644 --- a/dd-java-agent/instrumentation/spring-boot/src/test/groovy/SpringBootApplicationTest.groovy +++ b/dd-java-agent/instrumentation/spring-boot/src/test/groovy/SpringBootApplicationTest.groovy @@ -1,3 +1,5 @@ +import datadog.trace.api.ProcessTags + import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace import datadog.trace.agent.test.AgentTestRunner @@ -5,11 +7,15 @@ import datadog.trace.api.Config import org.springframework.beans.factory.InitializingBean import org.springframework.boot.SpringApplication +import static datadog.trace.api.config.GeneralConfig.EXPERIMENTAL_COLLECT_PROCESS_TAGS_ENABLED + class SpringBootApplicationTest extends AgentTestRunner { @Override protected void configurePreAgent() { super.configurePreAgent() + injectSysConfig(EXPERIMENTAL_COLLECT_PROCESS_TAGS_ENABLED, "true") } + static class BeanWhoTraces implements InitializingBean { @Override @@ -32,15 +38,19 @@ class SpringBootApplicationTest extends AgentTestRunner { } } }) + and: + def processTags = ProcessTags.getTagsForSerialization() + assert processTags.toString() =~ ".+,springboot.application:$expectedService,springboot.profile:$expectedProfile" context != null cleanup: context?.stop() where: - expectedService | args - "application-name-from-args" | new String[]{ - "--spring.application.name=application-name-from-args" + expectedService | expectedProfile | args + "application-name-from-args" | "prod" | new String[]{ + "--spring.application.name=application-name-from-args", + "--spring.profiles.active=prod,common", } - "application-name-from-properties" | new String[0] // will load from properties + "application-name-from-properties" | "default" | new String[0] // will load from properties } } diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/AgentTestRunner.groovy b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/AgentTestRunner.groovy index 029024f2a35..547431d7ace 100644 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/AgentTestRunner.groovy +++ b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/AgentTestRunner.groovy @@ -27,6 +27,7 @@ import datadog.trace.agent.tooling.bytebuddy.matcher.GlobalIgnores import datadog.trace.api.Config import datadog.trace.api.DDSpanId import datadog.trace.api.IdGenerationStrategy +import datadog.trace.api.ProcessTags import datadog.trace.api.StatsDClient import datadog.trace.api.TraceConfig import datadog.trace.api.WellKnownTags @@ -509,6 +510,7 @@ abstract class AgentTestRunner extends DDSpecification implements AgentBuilder.L ActiveSubsystems.APPSEC_ACTIVE = true } InstrumentationErrors.resetErrorCount() + ProcessTags.reset() } @Override diff --git a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java index 37931a666f5..0caa4a58cd0 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java @@ -241,7 +241,8 @@ public final class ConfigDefaults { static final int DEFAULT_TELEMETRY_DEPENDENCY_RESOLUTION_QUEUE_SIZE = 100000; static final Set DEFAULT_TRACE_EXPERIMENTAL_FEATURES_ENABLED = - new HashSet<>(asList("DD_TAGS", "DD_LOGS_INJECTION")); + new HashSet<>( + asList("DD_TAGS", "DD_LOGS_INJECTION", "DD_EXPERIMENTAL_COLLECT_PROCESS_TAGS_ENABLED")); static final boolean DEFAULT_TRACE_128_BIT_TRACEID_GENERATION_ENABLED = true; static final boolean DEFAULT_TRACE_128_BIT_TRACEID_LOGGING_ENABLED = true; diff --git a/dd-trace-api/src/main/java/datadog/trace/api/DDTags.java b/dd-trace-api/src/main/java/datadog/trace/api/DDTags.java index 7d165a33b59..1556fef030c 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/DDTags.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/DDTags.java @@ -98,4 +98,5 @@ public class DDTags { public static final String DECISION_MAKER_INHERITED = "_dd.dm.inherited"; public static final String DECISION_MAKER_SERVICE = "_dd.dm.service"; public static final String DECISION_MAKER_RESOURCE = "_dd.dm.resource"; + public static final String PROCESS_TAGS = "_dd.tags.process"; } diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java index e49baba90e0..adf23013d9b 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/GeneralConfig.java @@ -29,6 +29,9 @@ public final class GeneralConfig { @Deprecated // Use dd.tags instead public static final String GLOBAL_TAGS = "trace.global.tags"; + public static final String EXPERIMENTAL_COLLECT_PROCESS_TAGS_ENABLED = + "experimental.collect.tags.enabled"; + public static final String LOG_LEVEL = "log.level"; public static final String TRACE_DEBUG = "trace.debug"; public static final String TRACE_TRIAGE = "trace.triage"; diff --git a/dd-trace-core/src/main/java/datadog/trace/common/writer/ddagent/TraceMapper.java b/dd-trace-core/src/main/java/datadog/trace/common/writer/ddagent/TraceMapper.java index 3866a056b5e..b48e8adbc0c 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/writer/ddagent/TraceMapper.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/writer/ddagent/TraceMapper.java @@ -12,4 +12,5 @@ public interface TraceMapper extends RemoteMapper { UTF8BytesString SAMPLING_PRIORITY_KEY = UTF8BytesString.create(DDSpanContext.PRIORITY_SAMPLING_KEY); UTF8BytesString ORIGIN_KEY = UTF8BytesString.create(DDTags.ORIGIN_KEY); + UTF8BytesString PROCESS_TAGS_KEY = UTF8BytesString.create(DDTags.PROCESS_TAGS); } diff --git a/dd-trace-core/src/main/java/datadog/trace/common/writer/ddagent/TraceMapperV0_4.java b/dd-trace-core/src/main/java/datadog/trace/common/writer/ddagent/TraceMapperV0_4.java index 4033a23897b..a1d60164b82 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/writer/ddagent/TraceMapperV0_4.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/writer/ddagent/TraceMapperV0_4.java @@ -6,6 +6,7 @@ import datadog.communication.serialization.GrowableBuffer; import datadog.communication.serialization.Writable; import datadog.communication.serialization.msgpack.MsgPackWriter; +import datadog.trace.api.ProcessTags; import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import datadog.trace.common.writer.Payload; @@ -36,25 +37,35 @@ public TraceMapperV0_4() { private static final class MetaWriter implements MetadataConsumer { private Writable writable; - private boolean writeSamplingPriority; + private boolean firstSpanInChunk; + private boolean lastSpanInChunk; MetaWriter withWritable(Writable writable) { this.writable = writable; return this; } - MetaWriter withWriteSamplingPriority(final boolean writeSamplingPriority) { - this.writeSamplingPriority = writeSamplingPriority; + MetaWriter forFirstSpanInChunk(final boolean firstSpanInChunk) { + this.firstSpanInChunk = firstSpanInChunk; + return this; + } + + MetaWriter forLastSpanInChunk(final boolean lastSpanInChunk) { + this.lastSpanInChunk = lastSpanInChunk; return this; } @Override public void accept(Metadata metadata) { + final boolean writeSamplingPriority = firstSpanInChunk || lastSpanInChunk; + final UTF8BytesString processTags = + firstSpanInChunk ? ProcessTags.getTagsForSerialization() : null; int metaSize = metadata.getBaggage().size() + metadata.getTags().size() + (null == metadata.getHttpStatusCode() ? 0 : 1) + (null == metadata.getOrigin() ? 0 : 1) + + (null == processTags ? 0 : 1) + 1; int metricsSize = (writeSamplingPriority && metadata.hasSamplingPriority() ? 1 : 0) @@ -124,6 +135,10 @@ public void accept(Metadata metadata) { writable.writeUTF8(ORIGIN_KEY); writable.writeString(metadata.getOrigin(), null); } + if (processTags != null) { + writable.writeUTF8(PROCESS_TAGS_KEY); + writable.writeUTF8(processTags); + } for (Map.Entry entry : metadata.getTags().entrySet()) { String key = entry.getKey(); Object value = entry.getValue(); @@ -273,7 +288,8 @@ public void map(List> trace, final Writable writable) { span.processTagsAndBaggage( metaWriter .withWritable(writable) - .withWriteSamplingPriority(i == 0 || i == trace.size() - 1)); + .forFirstSpanInChunk(i == 0) + .forLastSpanInChunk(i == trace.size() - 1)); if (!metaStruct.isEmpty()) { /* 13 */ metaStructWriter.withWritable(writable).write(metaStruct); diff --git a/dd-trace-core/src/main/java/datadog/trace/common/writer/ddagent/TraceMapperV0_5.java b/dd-trace-core/src/main/java/datadog/trace/common/writer/ddagent/TraceMapperV0_5.java index 681a9a57693..786df12ecfc 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/writer/ddagent/TraceMapperV0_5.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/writer/ddagent/TraceMapperV0_5.java @@ -7,6 +7,7 @@ import datadog.communication.serialization.Writable; import datadog.communication.serialization.WritableFormatter; import datadog.communication.serialization.msgpack.MsgPackWriter; +import datadog.trace.api.ProcessTags; import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import datadog.trace.common.writer.Payload; @@ -78,7 +79,8 @@ public void map(final List> trace, final Writable writable span.processTagsAndBaggage( metaWriter .withWritable(writable) - .withWriteSamplingPriority(i == 0 || i == trace.size() - 1)); + .forLastSpanInChunk(i == 0) + .forLastSpanInChunk(i == trace.size() - 1)); /* 12 */ writeDictionaryEncoded(writable, span.getType()); } @@ -179,25 +181,35 @@ private List toList() { private final class MetaWriter implements MetadataConsumer { private Writable writable; - private boolean writeSamplingPriority; + private boolean firstSpanInChunk; + private boolean lastSpanInChunk; MetaWriter withWritable(final Writable writable) { this.writable = writable; return this; } - MetaWriter withWriteSamplingPriority(final boolean writeSamplingPriority) { - this.writeSamplingPriority = writeSamplingPriority; + MetaWriter forFirstSpanInChunk(final boolean firstSpanInChunk) { + this.firstSpanInChunk = firstSpanInChunk; + return this; + } + + MetaWriter forLastSpanInChunk(final boolean lastSpanInChunk) { + this.lastSpanInChunk = lastSpanInChunk; return this; } @Override public void accept(Metadata metadata) { + final boolean writeSamplingPriority = firstSpanInChunk || lastSpanInChunk; + final UTF8BytesString processTags = + firstSpanInChunk ? ProcessTags.getTagsForSerialization() : null; int metaSize = metadata.getBaggage().size() + metadata.getTags().size() + (null == metadata.getHttpStatusCode() ? 0 : 1) + (null == metadata.getOrigin() ? 0 : 1) + + (null == processTags ? 0 : 1) + 1; int metricsSize = (writeSamplingPriority && metadata.hasSamplingPriority() ? 1 : 0) @@ -234,6 +246,10 @@ public void accept(Metadata metadata) { writeDictionaryEncoded(writable, ORIGIN_KEY); writeDictionaryEncoded(writable, metadata.getOrigin()); } + if (null != processTags) { + writeDictionaryEncoded(writable, PROCESS_TAGS_KEY); + writeDictionaryEncoded(writable, processTags); + } for (Map.Entry entry : metadata.getTags().entrySet()) { String key = entry.getKey(); Object value = entry.getValue(); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index 70b96f2ff55..783bc02bfd2 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -7,6 +7,7 @@ import datadog.trace.api.DDTags; import datadog.trace.api.DDTraceId; import datadog.trace.api.Functions; +import datadog.trace.api.ProcessTags; import datadog.trace.api.cache.DDCache; import datadog.trace.api.cache.DDCaches; import datadog.trace.api.config.TracerConfig; @@ -874,7 +875,8 @@ public void processTagsAndBaggage( httpStatusCode == 0 ? null : HTTP_STATUSES.get(httpStatusCode), // Get origin from rootSpan.context getOrigin(), - longRunningVersion)); + longRunningVersion, + ProcessTags.getTagsForSerialization())); } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/Metadata.java b/dd-trace-core/src/main/java/datadog/trace/core/Metadata.java index f1f79454164..01054a91638 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/Metadata.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/Metadata.java @@ -17,6 +17,7 @@ public final class Metadata { private final boolean topLevel; private final CharSequence origin; private final int longRunningVersion; + private final UTF8BytesString processTags; public Metadata( long threadId, @@ -28,7 +29,8 @@ public Metadata( boolean topLevel, UTF8BytesString httpStatusCode, CharSequence origin, - int longRunningVersion) { + int longRunningVersion, + UTF8BytesString processTags) { this.threadId = threadId; this.threadName = threadName; this.httpStatusCode = httpStatusCode; @@ -39,6 +41,7 @@ public Metadata( this.topLevel = topLevel; this.origin = origin; this.longRunningVersion = longRunningVersion; + this.processTags = processTags; } public UTF8BytesString getHttpStatusCode() { @@ -84,4 +87,8 @@ public boolean hasSamplingPriority() { public int samplingPriority() { return samplingPriority; } + + public UTF8BytesString processTags() { + return processTags; + } } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessorFactory.java b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessorFactory.java index 154d7f54153..2812bdb8479 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessorFactory.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/tagprocessor/TagsPostProcessorFactory.java @@ -10,7 +10,7 @@ public final class TagsPostProcessorFactory { private static class Lazy { private static TagsPostProcessor create() { - final List processors = new ArrayList<>(4); + final List processors = new ArrayList<>(7); processors.add(new PeerServiceCalculator()); if (addBaseService) { processors.add(new BaseServiceAdder(Config.get().getServiceName())); @@ -52,6 +52,7 @@ public static void withAddBaseService(boolean enabled) { addBaseService = enabled; Lazy.instance = Lazy.create(); } + /** * Mostly used for test purposes. * diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/writer/TraceGenerator.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/writer/TraceGenerator.groovy index db3bd899805..4ce25337ce7 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/writer/TraceGenerator.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/common/writer/TraceGenerator.groovy @@ -4,6 +4,7 @@ import datadog.trace.api.DDSpanId import datadog.trace.api.DDTags import datadog.trace.api.DDTraceId import datadog.trace.api.IdGenerationStrategy +import datadog.trace.api.ProcessTags import datadog.trace.api.sampling.PrioritySampling import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString import datadog.trace.core.CoreSpan @@ -177,7 +178,8 @@ class TraceGenerator { this.samplingPriority = samplingPriority this.metadata = new Metadata(Thread.currentThread().getId(), UTF8BytesString.create(Thread.currentThread().getName()), tags, baggage, samplingPriority, measured, topLevel, - statusCode == 0 ? null : UTF8BytesString.create(Integer.toString(statusCode)), origin, 0) + statusCode == 0 ? null : UTF8BytesString.create(Integer.toString(statusCode)), origin, 0, + ProcessTags.tagsForSerialization) this.httpStatusCode = (short) statusCode } diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/writer/ddagent/TraceMapperV04PayloadTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/writer/ddagent/TraceMapperV04PayloadTest.groovy index 8cde97d61b2..f7d027f62d4 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/writer/ddagent/TraceMapperV04PayloadTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/common/writer/ddagent/TraceMapperV04PayloadTest.groovy @@ -3,9 +3,11 @@ package datadog.trace.common.writer.ddagent import datadog.communication.serialization.ByteBufferConsumer import datadog.communication.serialization.FlushingBuffer import datadog.communication.serialization.msgpack.MsgPackWriter +import datadog.trace.api.Config import datadog.trace.api.DD64bTraceId import datadog.trace.api.DDTags import datadog.trace.api.DDTraceId +import datadog.trace.api.ProcessTags import datadog.trace.bootstrap.instrumentation.api.Tags import datadog.trace.common.writer.Payload import datadog.trace.common.writer.TraceGenerator @@ -19,10 +21,13 @@ import org.msgpack.core.MessageUnpacker import java.nio.ByteBuffer import java.nio.channels.WritableByteChannel +import static datadog.trace.api.config.GeneralConfig.EXPERIMENTAL_COLLECT_PROCESS_TAGS_ENABLED import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.DD_MEASURED import static datadog.trace.common.writer.TraceGenerator.generateRandomTraces import static org.junit.jupiter.api.Assertions.assertEquals import static org.junit.jupiter.api.Assertions.assertFalse +import static org.junit.jupiter.api.Assertions.assertNotNull +import static org.junit.jupiter.api.Assertions.assertTrue import static org.msgpack.core.MessageFormat.FLOAT32 import static org.msgpack.core.MessageFormat.FLOAT64 import static org.msgpack.core.MessageFormat.INT16 @@ -170,6 +175,46 @@ class TraceMapperV04PayloadTest extends DDSpecification { verifier.verifyTracesConsumed() } + void 'test process tags serialization'() { + setup: + injectSysConfig(EXPERIMENTAL_COLLECT_PROCESS_TAGS_ENABLED, "true") + ProcessTags.reset() + assertNotNull(ProcessTags.tagsForSerialization) + def spans = (1..2).collect { + new TraceGenerator.PojoSpan( + 'service', + 'operation', + 'resource', + DDTraceId.ONE, + it, + -1L, + 123L, + 456L, + 0, + [:], + [:], + 'type', + false, + 0, + 0, + 'origin') + } + + def traces = [spans] + TraceMapperV0_4 traceMapper = new TraceMapperV0_4() + PayloadVerifier verifier = new PayloadVerifier(traces, traceMapper) + MsgPackWriter packer = new MsgPackWriter(new FlushingBuffer(20 << 10, verifier)) + + when: + packer.format(spans, traceMapper) + packer.flush() + + then: + verifier.verifyTracesConsumed() + cleanup: + ProcessTags.empty() + } + private static final class PayloadVerifier implements ByteBufferConsumer, WritableByteChannel { private final List> expectedTraces @@ -301,6 +346,10 @@ class TraceMapperV04PayloadTest extends DDSpecification { assertEquals(String.valueOf(expectedSpan.getHttpStatusCode()), entry.getValue()) } else if (DDTags.ORIGIN_KEY.equals(entry.getKey())) { assertEquals(expectedSpan.getOrigin(), entry.getValue()) + } else if (DDTags.PROCESS_TAGS.equals(entry.getKey())) { + assertTrue(Config.get().isExperimentalCollectProcessTagsEnabled()) + assertEquals(0, k) + assertEquals(ProcessTags.tagsForSerialization.toString(), entry.getValue()) } else { Object tag = expectedSpan.getTag(entry.getKey()) if (null != tag) { diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/writer/ddagent/TraceMapperV05PayloadTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/writer/ddagent/TraceMapperV05PayloadTest.groovy index f416b7fff5a..1c46f0a62f6 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/writer/ddagent/TraceMapperV05PayloadTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/common/writer/ddagent/TraceMapperV05PayloadTest.groovy @@ -3,13 +3,15 @@ package datadog.trace.common.writer.ddagent import datadog.communication.serialization.ByteBufferConsumer import datadog.communication.serialization.FlushingBuffer import datadog.communication.serialization.msgpack.MsgPackWriter +import datadog.trace.api.Config import datadog.trace.api.DDSpanId import datadog.trace.api.DDTags import datadog.trace.api.DDTraceId +import datadog.trace.api.ProcessTags import datadog.trace.api.sampling.PrioritySampling +import datadog.trace.bootstrap.instrumentation.api.Tags import datadog.trace.common.writer.Payload import datadog.trace.common.writer.TraceGenerator -import datadog.trace.bootstrap.instrumentation.api.Tags import datadog.trace.core.DDSpanContext import datadog.trace.test.util.DDSpecification import org.junit.Assert @@ -21,10 +23,12 @@ import java.nio.ByteBuffer import java.nio.channels.WritableByteChannel import java.util.concurrent.atomic.AtomicInteger +import static datadog.trace.api.config.GeneralConfig.EXPERIMENTAL_COLLECT_PROCESS_TAGS_ENABLED import static datadog.trace.bootstrap.instrumentation.api.InstrumentationTags.DD_MEASURED import static datadog.trace.common.writer.TraceGenerator.generateRandomTraces import static org.junit.Assert.assertEquals import static org.junit.Assert.assertFalse +import static org.junit.Assert.assertNotNull import static org.msgpack.core.MessageFormat.* class TraceMapperV05PayloadTest extends DDSpecification { @@ -129,6 +133,46 @@ class TraceMapperV05PayloadTest extends DDSpecification { 100 << 10 | 100 << 10 | 1000 | false } + void 'test process tags serialization'() { + setup: + injectSysConfig(EXPERIMENTAL_COLLECT_PROCESS_TAGS_ENABLED, "true") + ProcessTags.reset() + assertNotNull(ProcessTags.tagsForSerialization) + def spans = (1..2).collect { + new TraceGenerator.PojoSpan( + 'service', + 'operation', + 'resource', + DDTraceId.ONE, + it, + -1L, + 123L, + 456L, + 0, + [:], + [:], + 'type', + false, + 0, + 0, + 'origin') + } + + def traces = [spans] + TraceMapperV0_5 traceMapper = new TraceMapperV0_5() + PayloadVerifier verifier = new PayloadVerifier(traces, traceMapper) + MsgPackWriter packer = new MsgPackWriter(new FlushingBuffer(20 << 10, verifier)) + + when: + packer.format(spans, traceMapper) + packer.flush() + + then: + verifier.verifyTracesConsumed() + cleanup: + ProcessTags.empty() + } + private static final class PayloadVerifier implements ByteBufferConsumer, WritableByteChannel { private final List> expectedTraces @@ -203,7 +247,10 @@ class TraceMapperV05PayloadTest extends DDSpecification { } else if(DDTags.ORIGIN_KEY.equals(entry.getKey())) { assertEquals(expectedSpan.getOrigin(), entry.getValue()) - + } else if (DDTags.PROCESS_TAGS.equals(entry.getKey())) { + assertTrue(Config.get().isExperimentalCollectProcessTagsEnabled()) + assertEquals(0, k) + assertEquals(ProcessTags.tagsForSerialization.toString(), entry.getValue()) } else { Object tag = expectedSpan.getTag(entry.getKey()) if (null != tag) { diff --git a/dd-trace-core/src/traceAgentTest/groovy/TraceGenerator.groovy b/dd-trace-core/src/traceAgentTest/groovy/TraceGenerator.groovy index 9089f8b5a43..d3420c2116b 100644 --- a/dd-trace-core/src/traceAgentTest/groovy/TraceGenerator.groovy +++ b/dd-trace-core/src/traceAgentTest/groovy/TraceGenerator.groovy @@ -2,6 +2,7 @@ import datadog.trace.api.DDSpanId import datadog.trace.api.DDTags import datadog.trace.api.DDTraceId import datadog.trace.api.IdGenerationStrategy +import datadog.trace.api.ProcessTags import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString import datadog.trace.core.CoreSpan import datadog.trace.core.Metadata @@ -155,7 +156,8 @@ class TraceGenerator { this.type = type this.measured = measured this.metadata = new Metadata(Thread.currentThread().getId(), - UTF8BytesString.create(Thread.currentThread().getName()), tags, baggage, UNSET, measured, topLevel, null, null, 0) + UTF8BytesString.create(Thread.currentThread().getName()), tags, baggage, UNSET, measured, topLevel, null, null, 0, + ProcessTags.tagsForSerialization) } @Override diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index 0a6f84057b4..bf049d0543a 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -150,6 +150,7 @@ public static String getHostName() { private final boolean peerServiceDefaultsEnabled; private final Map peerServiceComponentOverrides; private final boolean removeIntegrationServiceNamesEnabled; + private final boolean experimentalCollectProcessTagsEnabled; private final Map peerServiceMapping; private final Map serviceMapping; private final Map tags; @@ -846,6 +847,8 @@ private Config(final ConfigProvider configProvider, final InstrumenterConfig ins // feature flag to remove fake services in v0 removeIntegrationServiceNamesEnabled = configProvider.getBoolean(TRACE_REMOVE_INTEGRATION_SERVICE_NAMES_ENABLED, false); + experimentalCollectProcessTagsEnabled = + configProvider.getBoolean(EXPERIMENTAL_COLLECT_PROCESS_TAGS_ENABLED, false); peerServiceMapping = configProvider.getMergedMap(TRACE_PEER_SERVICE_MAPPING); @@ -2091,6 +2094,10 @@ public Set getExperimentalFeaturesEnabled() { return experimentalFeaturesEnabled; } + public boolean isExperimentalCollectProcessTagsEnabled() { + return experimentalCollectProcessTagsEnabled; + } + public boolean isTraceEnabled() { return instrumenterConfig.isTraceEnabled(); } diff --git a/internal-api/src/main/java/datadog/trace/api/ProcessTags.java b/internal-api/src/main/java/datadog/trace/api/ProcessTags.java new file mode 100644 index 00000000000..92d6defc30e --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/ProcessTags.java @@ -0,0 +1,129 @@ +package datadog.trace.api; + +import datadog.trace.api.env.CapturedEnvironment; +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import datadog.trace.util.TraceUtils; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ProcessTags { + private static final Logger LOGGER = LoggerFactory.getLogger(ProcessTags.class); + private static boolean enabled = Config.get().isExperimentalCollectProcessTagsEnabled(); + + private static class Lazy { + static final Map TAGS = loadTags(); + static volatile UTF8BytesString serializedForm; + + private static Map loadTags() { + Map tags = new LinkedHashMap<>(); + if (enabled) { + try { + fillBaseTags(tags); + fillJbossTags(tags); + } catch (Throwable t) { + LOGGER.debug("Unable to calculate default process tags", t); + } + } + return tags; + } + + private static void insertSysPropIfPresent( + Map tags, String propKey, String tagKey) { + String value = System.getProperty(propKey); + if (value != null) { + tags.put(tagKey, value); + } + } + + private static boolean insertLastPathSegmentIfPresent( + Map tags, String path, String tagKey) { + if (path == null || path.isEmpty()) { + return false; + } + try { + final Path p = Paths.get(path).getFileName(); + if (p != null) { + tags.put(tagKey, p.toString()); + return true; + } + } catch (Throwable ignored) { + } + return false; + } + + private static void fillBaseTags(Map tags) { + final CapturedEnvironment.ProcessInfo processInfo = + CapturedEnvironment.get().getProcessInfo(); + if (processInfo.mainClass != null) { + tags.put("entrypoint.name", processInfo.mainClass); + } + if (processInfo.jarFile != null) { + final String jarName = processInfo.jarFile.getName(); + tags.put("entrypoint.name", jarName.substring(0, jarName.length() - 4)); // strip .jar + insertLastPathSegmentIfPresent(tags, processInfo.jarFile.getParent(), "entrypoint.basedir"); + } + + insertLastPathSegmentIfPresent(tags, System.getProperty("user.dir"), "entrypoint.workdir"); + } + + private static void fillJbossTags(Map tags) { + if (insertLastPathSegmentIfPresent( + tags, System.getProperty("jboss.home.dir"), "jboss.home")) { + insertSysPropIfPresent(tags, "jboss.server.name", "server.name"); + tags.put( + "jboss.mode", + System.getProperties().containsKey("[Standalone]") ? "standalone" : "domain"); + } + } + + static synchronized UTF8BytesString calculateSerializedForm() { + if (serializedForm == null && !TAGS.isEmpty()) { + serializedForm = + UTF8BytesString.create( + TAGS.entrySet().stream() + .map(entry -> entry.getKey() + ":" + TraceUtils.normalizeTag(entry.getValue())) + .collect(Collectors.joining(","))); + } + return serializedForm; + } + } + + private ProcessTags() {} + + // need to be synchronized on writing. As optimization, it does not need to be sync on read. + public static synchronized void addTag(String key, String value) { + if (enabled) { + Lazy.TAGS.put(key, value); + Lazy.serializedForm = null; + } + } + + public static UTF8BytesString getTagsForSerialization() { + if (!enabled) { + return null; + } + final UTF8BytesString serializedForm = Lazy.serializedForm; + if (serializedForm != null) { + return serializedForm; + } + return Lazy.calculateSerializedForm(); + } + + /** Visible for testing. */ + static void empty() { + Lazy.TAGS.clear(); + Lazy.serializedForm = null; + } + + /** Visible for testing. */ + static void reset() { + empty(); + enabled = Config.get().isExperimentalCollectProcessTagsEnabled(); + Lazy.TAGS.putAll(Lazy.loadTags()); + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/env/CapturedEnvironment.java b/internal-api/src/main/java/datadog/trace/api/env/CapturedEnvironment.java index 5bbb2744531..50e4d67fc5d 100644 --- a/internal-api/src/main/java/datadog/trace/api/env/CapturedEnvironment.java +++ b/internal-api/src/main/java/datadog/trace/api/env/CapturedEnvironment.java @@ -4,6 +4,7 @@ import de.thetaphi.forbiddenapis.SuppressForbidden; import java.io.File; import java.util.HashMap; +import java.util.Locale; import java.util.Map; /** @@ -13,12 +14,54 @@ */ public class CapturedEnvironment { + public static class ProcessInfo { + public String mainClass; + public File jarFile; + + @SuppressForbidden + public ProcessInfo() { + // Besides "sun.java.command" property is not an standard, all main JDKs has set this + // property. + // Tested on: + // - OracleJDK, OpenJDK, AdoptOpenJDK, IBM JDK, Azul Zulu JDK, Amazon Coretto JDK + final String command = System.getProperty("sun.java.command"); + if (command == null || command.isEmpty()) { + return; + } + + final String[] split = command.trim().split(" "); + if (split.length == 0 || split[0].isEmpty()) { + return; + } + + final String candidate = split[0]; + if (candidate.toLowerCase(Locale.ROOT).endsWith(".jar")) { + jarFile = new File(candidate); + } else { + mainClass = candidate; + } + } + + /** + * Visible for testing + * + * @param mainClass + * @param jarFile + */ + ProcessInfo(String mainClass, File jarFile) { + this.mainClass = mainClass; + this.jarFile = jarFile; + } + } + private static final CapturedEnvironment INSTANCE = new CapturedEnvironment(); private final Map properties; + private ProcessInfo processInfo; CapturedEnvironment() { properties = new HashMap<>(); + processInfo = new ProcessInfo(); properties.put(GeneralConfig.SERVICE_NAME, autodetectServiceName()); } @@ -26,6 +69,10 @@ public Map getProperties() { return properties; } + public ProcessInfo getProcessInfo() { + return processInfo; + } + // Testing purposes static void useFixedEnv(final Map props) { INSTANCE.properties.clear(); @@ -35,6 +82,15 @@ static void useFixedEnv(final Map props) { } } + /** + * For testing purposes. + * + * @param processInfo + */ + static void useFixedProcessInfo(final ProcessInfo processInfo) { + INSTANCE.processInfo = processInfo; + } + /** * Returns autodetected service name based on the java process command line. Typically, the * autodetection will return either the JAR filename or the java main class. @@ -47,29 +103,12 @@ private String autodetectServiceName() { return siteName; } - // Besides "sun.java.command" property is not an standard, all main JDKs has set this property. - // Tested on: - // - OracleJDK, OpenJDK, AdoptOpenJDK, IBM JDK, Azul Zulu JDK, Amazon Coretto JDK - return extractJarOrClass(System.getProperty("sun.java.command")); - } - - @SuppressForbidden - private String extractJarOrClass(final String command) { - if (command == null || command.equals("")) { - return null; + if (processInfo.jarFile != null) { + final String jarName = processInfo.jarFile.getName(); + return jarName.substring(0, jarName.length() - 4); // strip .jar + } else { + return processInfo.mainClass; } - - final String[] split = command.trim().split(" "); - if (split.length == 0 || split[0].equals("")) { - return null; - } - - final String candidate = split[0]; - if (candidate.endsWith(".jar")) { - return new File(candidate).getName().replace(".jar", ""); - } - - return candidate; } public static CapturedEnvironment get() { diff --git a/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy index 8dafd91f4a2..9149b0f4a01 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy @@ -1938,7 +1938,7 @@ class ConfigTest extends DDSpecification { def config = new Config() then: - config.experimentalFeaturesEnabled == ["DD_TAGS", "DD_LOGS_INJECTION"].toSet() + config.experimentalFeaturesEnabled == ["DD_TAGS", "DD_LOGS_INJECTION", "DD_EXPERIMENTAL_COLLECT_PROCESS_TAGS_ENABLED"].toSet() } def "detect if agent is configured using default values"() { diff --git a/internal-api/src/test/groovy/datadog/trace/api/ProcessTagsForkedTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/ProcessTagsForkedTest.groovy new file mode 100644 index 00000000000..cc962b048f0 --- /dev/null +++ b/internal-api/src/test/groovy/datadog/trace/api/ProcessTagsForkedTest.groovy @@ -0,0 +1,91 @@ +package datadog.trace.api + +import datadog.trace.api.env.CapturedEnvironment +import datadog.trace.test.util.DDSpecification + +import java.nio.file.Paths + +import static datadog.trace.api.config.GeneralConfig.EXPERIMENTAL_COLLECT_PROCESS_TAGS_ENABLED + +class ProcessTagsForkedTest extends DDSpecification { + + def originalProcessInfo + + def setup() { + originalProcessInfo = CapturedEnvironment.get().getProcessInfo() + ProcessTags.reset() + } + + def cleanup() { + CapturedEnvironment.useFixedProcessInfo(originalProcessInfo) + } + + def 'should load default tags for jar #jar and main class #cls'() { + given: + injectSysConfig(EXPERIMENTAL_COLLECT_PROCESS_TAGS_ENABLED, "true") + CapturedEnvironment.useFixedProcessInfo(new CapturedEnvironment.ProcessInfo(cls, jar)) + ProcessTags.reset() + def tags = ProcessTags.getTagsForSerialization() + expect: + tags =~ expected + where: + jar | cls | expected + Paths.get("my test", "my.jar").toFile() | null | "entrypoint.name:my,entrypoint.basedir:my_test,entrypoint.workdir:[^,]+" + Paths.get("my.jar").toFile() | null | "entrypoint.name:my,entrypoint.workdir:[^,]+" + null | "com.test.Main" | "entrypoint.name:com.test.main,entrypoint.workdir:[^,]+" + null | null | "entrypoint.workdir:[^,]+" + } + + def 'should load default tags jboss (mode #mode)'() { + setup: + injectSysConfig(EXPERIMENTAL_COLLECT_PROCESS_TAGS_ENABLED, "true") + if (jbossHome != null) { + System.setProperty("jboss.home.dir", jbossHome) + } + System.setProperty(mode, "") // i.e. -D[Standalone] + System.setProperty("jboss.server.name", serverName) + when: + CapturedEnvironment.useFixedProcessInfo(new CapturedEnvironment.ProcessInfo(null, new File("/somewhere/jboss-modules.jar"))) + ProcessTags.reset() + def tags = ProcessTags.getTagsForSerialization() + then: + assert tags =~ expected + cleanup: + System.clearProperty(mode) + System.clearProperty("jboss.home.dir") + System.clearProperty("jboss.server.name") + where: + jbossHome | mode | serverName | expected + "/opt/jboss/myserver" | "[Standalone]" | "standalone" | "entrypoint.name:jboss-modules,entrypoint.basedir:somewhere,entrypoint.workdir:.+,jboss.home:myserver,server.name:standalone,jboss.mode:standalone" + "/opt/jboss/myserver" | "[server1:12345]" | "server1" | "entrypoint.name:jboss-modules,entrypoint.basedir:somewhere,entrypoint.workdir:.+,jboss.home:myserver,server.name:server1,jboss.mode:domain" + null | "[Standalone]" | "standalone" | "entrypoint.name:jboss-modules,entrypoint.basedir:somewhere,entrypoint.workdir:[^,]+" // don't expect jboss tags since home is missing + } + + def 'should not calculate process tags by default'() { + when: + ProcessTags.reset() + def processTags = ProcessTags.tagsForSerialization + then: + assert !ProcessTags.enabled + assert processTags == null + when: + ProcessTags.addTag("test", "value") + then: + assert ProcessTags.tagsForSerialization == null + } + + def 'should lazily recalculate when a tag is added'() { + setup: + injectSysConfig(EXPERIMENTAL_COLLECT_PROCESS_TAGS_ENABLED, "true") + ProcessTags.reset() + when: + def processTags = ProcessTags.tagsForSerialization + then: + assert ProcessTags.enabled + assert processTags != null + when: + ProcessTags.addTag("test", "value") + then: + assert ProcessTags.tagsForSerialization.toString() == "$processTags,test:value" + } +}