Skip to content
Merged
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
Expand Up @@ -9,8 +9,10 @@
import datadog.communication.serialization.GrowableBuffer;
import datadog.communication.serialization.StreamingBuffer;
import datadog.trace.api.Config;
import datadog.trace.api.ProcessTags;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;

Expand All @@ -31,9 +33,25 @@ private OtlpResourceProto() {}
"telemetry.sdk.version",
"telemetry.sdk.language"));

/** Prefix applied to {@code datadog.runtime_id} and process-tag resource attributes. */
private static final String DATADOG_PREFIX = "datadog.";

/** Vendor-neutral resource (no {@code datadog.*}). Used by the OTLP trace/metric export. */
public static final byte[] RESOURCE_MESSAGE = buildResourceMessage(Config.get());

/**
* Resource that additionally carries {@code datadog.runtime_id} and process tags (each prefixed
* {@code datadog.}). Used by the default-mode SDK trace-metrics export; omitted in OTel-semantics
* mode.
*/
public static final byte[] RESOURCE_MESSAGE_WITH_DATADOG_ATTRS =
buildResourceMessage(Config.get(), true);
Comment thread
mhlidd marked this conversation as resolved.

static byte[] buildResourceMessage(Config config) {
return buildResourceMessage(config, false);
}

static byte[] buildResourceMessage(Config config, boolean includeDatadogResourceAttributes) {
GrowableBuffer buf = new GrowableBuffer(512);

String serviceName = config.getServiceName();
Expand All @@ -47,6 +65,12 @@ static byte[] buildResourceMessage(Config config) {
if (!version.isEmpty()) {
writeResourceAttribute(buf, "service.version", version);
}
if (config.isReportHostName()) {
String hostName = config.getHostName();
if (hostName != null && !hostName.isEmpty()) {
writeResourceAttribute(buf, "host.name", hostName);
}
}
writeResourceAttribute(buf, "telemetry.sdk.name", "datadog");
writeResourceAttribute(buf, "telemetry.sdk.version", TRACER_VERSION);
writeResourceAttribute(buf, "telemetry.sdk.language", "java");
Expand All @@ -61,6 +85,10 @@ static byte[] buildResourceMessage(Config config) {
}
});

if (includeDatadogResourceAttributes) {
writeDatadogResourceAttributes(buf, config);
}

OtlpProtoBuffer protobuf = new OtlpProtoBuffer(buf.capacity());
int numBytes = protobuf.recordMessage(buf, 1);
byte[] resourceMessage = new byte[numBytes];
Expand All @@ -69,6 +97,24 @@ static byte[] buildResourceMessage(Config config) {
return resourceMessage;
}

private static void writeDatadogResourceAttributes(StreamingBuffer buf, Config config) {
String runtimeId = config.getRuntimeId();
if (runtimeId != null && !runtimeId.isEmpty()) {
writeResourceAttribute(buf, DATADOG_PREFIX + "runtime_id", runtimeId);
}
// Process tags arrive as "key:value" pairs; emit each as datadog.<key> = value.
List<String> processTags = ProcessTags.getTagsAsStringList();
if (processTags != null) {
for (String tag : processTags) {
int colon = tag.indexOf(':');
if (colon > 0) {
writeResourceAttribute(
buf, DATADOG_PREFIX + tag.substring(0, colon), tag.substring(colon + 1));
}
}
}
}

private static void writeResourceAttribute(StreamingBuffer buf, String key, String value) {
writeTag(buf, 1, LEN_WIRE_TYPE);
writeAttribute(buf, STRING_ATTRIBUTE, key, value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ public final class OtlpMetricsProtoCollector extends OtlpMetricsCollector

private final boolean forceHistogramDelta;

// resource chunk prepended to every payload; lets callers pick the plain vendor-neutral resource
// or the datadog-attrs variant (datadog.runtime_id / process tags)
private final byte[] resourceMessage;

private long startNanos;
private long endNanos;

Expand All @@ -68,9 +72,15 @@ public OtlpMetricsProtoCollector(TimeSource timeSource) {
}

OtlpMetricsProtoCollector(TimeSource timeSource, boolean forceHistogramDelta) {
this(timeSource, forceHistogramDelta, RESOURCE_MESSAGE);
}

OtlpMetricsProtoCollector(
TimeSource timeSource, boolean forceHistogramDelta, byte[] resourceMessage) {
this.timeSource = timeSource;
this.endNanos = timeSource.getCurrentTimeNanos();
this.forceHistogramDelta = forceHistogramDelta;
this.resourceMessage = resourceMessage;
}

/**
Expand Down Expand Up @@ -190,7 +200,7 @@ private OtlpPayload completePayload() {
}

// prepend the canned resource chunk
payloadBytes += protobuf.recordMessage(RESOURCE_MESSAGE);
payloadBytes += protobuf.recordMessage(resourceMessage);

// finally prepend the total length of all collected chunks
protobuf.recordMessage(buf, 1, payloadBytes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import datadog.trace.common.metrics.AggregateEntry;
import datadog.trace.common.metrics.MetricWriter;
import datadog.trace.core.otlp.common.OtlpPayload;
import datadog.trace.core.otlp.common.OtlpResourceProto;
import datadog.trace.core.otlp.common.OtlpSender;
import java.util.ArrayList;
import java.util.List;
Expand Down Expand Up @@ -56,9 +57,9 @@ public final class OtlpStatsMetricWriter implements MetricWriter {
@Nullable private final OtlpSender sender;
private final boolean otelSemanticsMode;

// own single-thread collector; forced to DELTA since trace-stats buckets are per-interval deltas
private final OtlpMetricsProtoCollector collector =
new OtlpMetricsProtoCollector(SystemTimeSource.INSTANCE, true);
// own single-thread collector; forced to DELTA since trace-stats buckets are per-interval deltas.
// Initialized in the constructor so the resource chunk can be chosen per semantics mode.
private final OtlpMetricsProtoCollector collector;

// data points snapshotted during add(), replayed through the visitor in finishBucket()
private final List<PendingPoint> pending = new ArrayList<>();
Expand Down Expand Up @@ -98,6 +99,14 @@ public OtlpStatsMetricWriter(Config config) {
OtlpStatsMetricWriter(@Nullable OtlpSender sender, boolean otelSemanticsMode) {
this.sender = sender;
this.otelSemanticsMode = otelSemanticsMode;
// Default mode carries datadog.runtime_id / process tags on the Resource; OTel-semantics mode
// uses the plain vendor-neutral resource (no datadog.*).
byte[] resourceMessage =
otelSemanticsMode
? OtlpResourceProto.RESOURCE_MESSAGE
: OtlpResourceProto.RESOURCE_MESSAGE_WITH_DATADOG_ATTRS;
this.collector =
new OtlpMetricsProtoCollector(SystemTimeSource.INSTANCE, true, resourceMessage);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import static datadog.trace.api.config.GeneralConfig.SERVICE_NAME;
import static datadog.trace.api.config.GeneralConfig.TAGS;
import static datadog.trace.api.config.GeneralConfig.VERSION;
import static datadog.trace.api.config.TracerConfig.TRACE_REPORT_HOSTNAME;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.google.protobuf.CodedInputStream;
Expand All @@ -16,6 +18,7 @@
import java.util.Map;
import java.util.Properties;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
Expand Down Expand Up @@ -99,6 +102,11 @@ static Stream<Arguments> resourceMessageCases() {
"service.name", "my-service",
"region", "us-east",
"team", "platform")),
// report-hostname enabled: host.name written with the detected hostname
Arguments.of(
"report-hostname enabled",
props(SERVICE_NAME, "my-service", TRACE_REPORT_HOSTNAME, "true"),
attrs("service.name", "my-service", "host.name", Config.get().getHostName())),
Comment on lines +105 to +109
// all config values set together; telemetry.sdk.* keys in tags must be ignored
Arguments.of(
"service, env, version, and tags all set",
Expand Down Expand Up @@ -147,6 +155,30 @@ void testBuildResourceMessage(
assertEquals(expectedAttributes, actualAttributes, "For case: " + caseName);
}

/**
* The datadog-attrs variant ({@code buildResourceMessage(config, true)}) carries {@code
* datadog.runtime_id}; the plain variant omits it. (Process tags are emitted only when the
* experimental process-tag propagation is enabled, so they aren't asserted here.)
*/
@Test
void datadogResourceAttributesVariantCarriesRuntimeId() throws IOException {
Config config = Config.get(props(SERVICE_NAME, "my-service"));

Map<String, String> withDatadog =
parseResourceAttributes(OtlpResourceProto.buildResourceMessage(config, true));
Map<String, String> plain =
parseResourceAttributes(OtlpResourceProto.buildResourceMessage(config, false));

assertTrue(
withDatadog.containsKey("datadog.runtime_id"),
"datadog-attrs variant carries datadog.runtime_id");
assertEquals(
config.getRuntimeId(),
withDatadog.get("datadog.runtime_id"),
"runtime id matches the config value");
assertFalse(plain.containsKey("datadog.runtime_id"), "plain variant omits datadog.runtime_id");
}

// ── parsing helpers ───────────────────────────────────────────────────────

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,35 @@ private static DecodedMetric decode(byte[] payload) throws IOException {
return metric;
}

/**
* Decodes the {@code Resource.attributes} ({@code ResourceMetrics.resource = 1} → {@code
* Resource.attributes = 1}) into a key→value map, for asserting the {@code datadog.*} resource
* attributes emitted in default mode.
*/
private static Map<String, Object> decodeResourceAttributes(byte[] payload) throws IOException {
CodedInputStream metricsData = CodedInputStream.newInstance(payload);
metricsData.readTag(); // MetricsData.resource_metrics = 1
CodedInputStream resourceMetrics = metricsData.readBytes().newCodedInput();
Map<String, Object> attrs = new HashMap<>();
while (!resourceMetrics.isAtEnd()) {
int tag = resourceMetrics.readTag();
if (WireFormat.getTagFieldNumber(tag) == 1) { // Resource
CodedInputStream resource = resourceMetrics.readBytes().newCodedInput();
while (!resource.isAtEnd()) {
int rtag = resource.readTag();
if (WireFormat.getTagFieldNumber(rtag) == 1) { // KeyValue attributes
readKeyValue(resource.readBytes().newCodedInput(), attrs);
} else {
resource.skipField(rtag);
}
}
} else {
resourceMetrics.skipField(tag);
}
}
return attrs;
}

private static DecodedMetric parseScopeMetrics(CodedInputStream scopeMetrics) throws IOException {
DecodedMetric metric = null;
while (!scopeMetrics.isAtEnd()) {
Expand Down Expand Up @@ -504,4 +533,44 @@ void snapshotsEntryDataBeforeAggregatorClearsIt() throws IOException {
assertEquals(
1L, dp.attributes.get("datadog.span.top_level"), "all pre-clear hits were top-level");
}

// ── resource attributes (datadog.runtime_id / process tags) ────────────────

@Test
void defaultModeResourceCarriesRuntimeId() throws IOException {
// runtime-id is enabled by default, so default-mode payloads carry datadog.runtime_id on the
// Resource.
CapturingSender sender = new CapturingSender();
OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender, false);
writer.startBucket(1, SECONDS.toNanos(1_700_000_000L), SECONDS.toNanos(10));
writer.add(okEntry(SECONDS.toNanos(1), 1));
writer.finishBucket();

Map<String, Object> resourceAttrs = decodeResourceAttributes(sender.lastPayload);
assertTrue(
resourceAttrs.containsKey("datadog.runtime_id"),
"default mode resource carries datadog.runtime_id");
Object runtimeId = resourceAttrs.get("datadog.runtime_id");
assertNotNull(runtimeId, "runtime id value present");
assertFalse(runtimeId.toString().isEmpty(), "runtime id value non-empty");
}

@Test
void otelSemanticsModeResourceOmitsDatadogAttributes() throws IOException {
CapturingSender sender = new CapturingSender();
OtlpStatsMetricWriter writer = new OtlpStatsMetricWriter(sender, true);
writer.startBucket(1, SECONDS.toNanos(1_700_000_000L), SECONDS.toNanos(10));
writer.add(okEntry(SECONDS.toNanos(1), 1));
writer.finishBucket();

Map<String, Object> resourceAttrs = decodeResourceAttributes(sender.lastPayload);
assertFalse(
resourceAttrs.containsKey("datadog.runtime_id"),
"otel-semantics mode resource omits datadog.runtime_id");
for (String key : resourceAttrs.keySet()) {
assertFalse(
key.startsWith("datadog."),
"otel-semantics mode resource has no datadog.* attrs: " + key);
}
}
}