Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3ab5862
Set known tags by id on TagMap and spans
dougqh Oct 9, 2026
4bf02bb
Ignore unknown ids when clearing a tag by id; fix a comment wrap
dougqh Oct 9, 2026
a2829d1
Precheck and reuse the box in long-valued setters
dougqh Oct 9, 2026
2d87cde
Keep peer.port off the id path
dougqh Oct 9, 2026
7a017a5
Merge set-by-id (#12715) onto master
dougqh Oct 9, 2026
0401a43
Mark intercepted tags with an id bit, via a tracer overlay
dougqh Oct 9, 2026
8949a57
Dispatch TagInterceptor on tag ids
dougqh Oct 9, 2026
cfe2368
Set tags on spans by id first
dougqh Oct 9, 2026
38c9947
Look up tag names by serial in an array, not a switch
dougqh Oct 9, 2026
3545c15
Point stale comments at the tracer overlay and the intercepted bit
dougqh Oct 9, 2026
1642cc9
Index the split-by-tags table by serial, sized to every known tag
dougqh Oct 9, 2026
b9dd573
Check for a known tag id with a range check, not a name lookup
dougqh Oct 9, 2026
75d3fbc
Pin interceptTag(long) above C2's inlining size
dougqh Oct 9, 2026
ecb4f73
Tighten comments on the id-keyed interceptor
dougqh Oct 9, 2026
3f65031
Treat overlay tags as intercepted; tidy from review
dougqh Oct 9, 2026
69d7849
Reject shared-name tag ids on the id-keyed TagMap and span paths
dougqh Oct 9, 2026
46c3b47
Test rejection of shared-name tag ids
dougqh Oct 9, 2026
ad2fa97
Build custom tags by name and known tags by id
dougqh Oct 10, 2026
3a269e1
Resolve direction-dependent tag names by the span's kind
dougqh Oct 9, 2026
d509119
Key shared-name tags by direction now that resolution knows it
dougqh Oct 10, 2026
628da33
Merge master into id-tag-interceptor (#12817)
dougqh Oct 10, 2026
cc43136
Merge id-tag-interceptor (#12817) into tag-registry-direction-resolution
dougqh Oct 10, 2026
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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
/SECURITY.md @DataDog/apm-java
/SUPPORT.md @DataDog/apm-java
/tag-conventions.yaml @DataDog/apm-java @DataDog/apm-lang-platform-java @DataDog/apm-sdk-capabilities
/tag-conventions-java.yaml @DataDog/apm-java @DataDog/apm-lang-platform-java @DataDog/apm-sdk-capabilities
/dd-smoke-tests/README.md @DataDog/apm-java
/docs/ @DataDog/apm-java

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
Expand All @@ -20,12 +21,18 @@ abstract class GenerateKnownTagsTask : DefaultTask() {
@get:PathSensitive(PathSensitivity.NONE)
abstract val tagConventionsFile: RegularFileProperty

@get:InputFile
@get:Optional
@get:PathSensitive(PathSensitivity.NONE)
abstract val tracerOverlayFile: RegularFileProperty

@get:OutputDirectory abstract val destinationDirectory: DirectoryProperty

@TaskAction
fun generate() {
val outDir = destinationDirectory.get().asFile
TagRegistryGenerator.generate(tagConventionsFile.get().asFile, outDir)
TagRegistryGenerator.generate(
tagConventionsFile.get().asFile, outDir, tracerOverlayFile.orNull?.asFile)
logger.lifecycle("tag-registry: generated -> $outDir")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import java.util.Locale
/**
* Emits the generated `KnownTags.java` from a [TagRegistry]. Public API first — per-tag
* `<X>_NAME` (string) + `<X>_ID` (encoded long, literal) couplets with a trailing `// makeTagId(...)`
* derivation comment — then the package-private `<X>_SERIAL_NUM` constants, the
* `StringIndex.EmbeddingSupport` keyOf table and the resolver's name switches.
* derivation comment — then the `<X>_SERIAL_NUM` constants, the
* `StringIndex.EmbeddingSupport` keyOf table, the `NAMES_BY_SERIAL` array behind `nameOf`, and the
* resolver's `openTelemetryNameOf` switch.
*/
object KnownTagsEmitter {

Expand Down Expand Up @@ -64,10 +65,27 @@ object KnownTagsEmitter {
fun otelNameC(identity: TagConventions.TagIdentity) = otelNameOfConst.getValue(identity)

val order = reg.tags.map { it.identity } // stable emit order
// keyOf has no direction argument, so omit shared names and let them resolve to 0 (unknown).
// keyOf(name) has no direction, so shared names are left out here and resolve to 0 (unknown);
// they are added below, marked, for keyOf(name, direction).
val keyOfOrder = reg.tags.filter { it.sharedNameDirection == null }.map { it.identity }
// canonical name -> OpenTelemetry name, for the reverse (openTelemetryNameOf) switch.
val otelName = reg.tags.mapNotNull { t -> t.otelName?.let { t.identity to it } }.toMap()
// Names whose tag depends on the span's direction: a shared Datadog name (one tag per direction)
// and a direction-scoped OpenTelemetry name. keyOf(name) resolves none of them; keyOf(name,
// direction) switches on each one's tag per direction.
val directional = sortedMapOf<String, MutableMap<TagConventions.Direction, TagConventions.TagIdentity>>()
// Shared Datadog names, marked in the keyOf table so a lookup by that name alone can probe its tags.
val sharedNames = sortedSetOf<String>()
for (t in reg.tags) {
t.sharedNameDirection?.let {
directional.getOrPut(t.ddName) { mutableMapOf() }[it] = t.identity
sharedNames.add(t.ddName)
}
t.otelDirection?.let { directional.getOrPut(t.declaredOtelName!!) { mutableMapOf() }[it] = t.identity }
}
check(directional.keys.none { name -> keyOfOrder.any { it.ddName == name } || name in otelName.values }) {
"a direction-dependent name is also direction-free: ${directional.keys}"
}
return buildString {
// Public API first (name + encoded id couplets), so readers see the useful parts up top; the
// serial ids and keyOf/resolver machinery follow below. Derivation is in the trailing comment.
Expand Down Expand Up @@ -101,16 +119,22 @@ object KnownTagsEmitter {
}
append("// makeTagId(serial=${t.serial})")
if (t.traceLevel) append(" + trace-level")
if (t.intercepted) append(" + intercepted")
if (t.sharedNameDirection != null) append(" + shared-name")
if (t.otelName != null) append(" -> ${escape(t.otelName)}")
appendLine(" <${escape(t.required)}>")
appendLine()
}

// Serial numbers (globalSerial per tag) — package-private, consumed by the resolver switch.
// Serial numbers (globalSerial per tag) — public, so a switch on KnownTagCodec.serialNum outside
// this package (the resolver here, TagInterceptor in core) can name them as case labels.
appendLine(" // ---- serial numbers ----")
for (t in reg.tags) {
appendLine(" static final int ${serialC(t.identity)} = ${t.serial};")
appendLine(" public static final int ${serialC(t.identity)} = ${t.serial};")
}
// Every known tag's serial is below SERIAL_LIMIT; serial 0 is no tag.
val serialLimit = (reg.tags.maxOfOrNull { it.serial } ?: 0) + 1
appendLine(" public static final int SERIAL_LIMIT = $serialLimit;")

// OpenTelemetry name -> canonical tag name. Validation ensures aliases are distinct from all
// canonical names. Sort by OTel name to keep output deterministic.
Expand All @@ -132,6 +156,7 @@ object KnownTagsEmitter {
otelByCanonical.forEach { (otel, _) ->
appendLine(" \"${escape(otel)}\",")
}
directional.keys.forEach { appendLine(" \"${escape(it)}\",") }
appendLine(
"""
};
Expand All @@ -142,6 +167,11 @@ object KnownTagsEmitter {
otelByCanonical.forEach { (_, canonical) ->
appendLine(" ${idC(canonical)},")
}
// A direction-dependent name's value is not an id but a marker saying which kind it is.
for (name in directional.keys) {
val marker = if (name in sharedNames) "SHARED_DATADOG_NAME_SENTINEL" else "DIRECTION_SCOPED_OTEL_NAME_SENTINEL"
appendLine(" KnownTagCodec.$marker, // $name")
}
// Resolver. KnownTagCodec.Installed links to this field directly, so merely resolving a tag
// name initializes this class -- there is no registration step and no ordering to get wrong.
appendLine(
Expand All @@ -163,6 +193,21 @@ object KnownTagsEmitter {
KEYOF_IDS = ids;
}

/**
* Each tag's Datadog name, indexed by serial; slot 0 (no tag) is null. An array rather than a
* switch keeps nameOf small enough to inline at every caller.
*/
static final String[] NAMES_BY_SERIAL = {
null,
""".trimIndent()
)
for (t in reg.tags.sortedBy { it.serial }) {
appendLine(" ${nameExpr(t)},")
}
appendLine(
"""
};

/**
* The registry's name&harr;id tables, as a {@link KnownTagCodec.Resolver}. {@code KnownTagCodec}
* reads this field from its own holder, so the two classes complete each other: the codec owns
Expand All @@ -172,39 +217,33 @@ object KnownTagsEmitter {
new KnownTagCodec.Resolver() {
@Override
public String nameOf(long tagId) {
switch (KnownTagCodec.serialNum(tagId)) {
""".trimIndent()
)
for (t in reg.tags) {
appendLine(
"""
case ${serialC(t.identity)}:
return ${nameExpr(t)};
""".trimIndent()
)
}
// openTelemetryNameOf: canonical id -> OTel-namespace name, null when the tag has none. The
// caller (a serializer) owns any fall-back-to-Datadog-name policy; this stays a pure lookup.
appendLine(
"""
default:
return null;
}
int serial = KnownTagCodec.serialNum(tagId);
return serial < NAMES_BY_SERIAL.length ? NAMES_BY_SERIAL[serial] : null;
}

// openTelemetryNameOf: id -> OTel-namespace name on spans of a direction, null when
// the tag has none there. The caller (a serializer) owns any fall-back-to-Datadog-name
// policy.
@Override
public String openTelemetryNameOf(long tagId) {
public String openTelemetryNameOf(long tagId, int direction) {
switch (KnownTagCodec.serialNum(tagId)) {
""".trimIndent()
)
for (identity in order) {
if (otelName[identity] == null) continue
appendLine(
"""
case ${serialC(identity)}:
return ${otelNameC(identity)};
""".trimIndent()
)
for (t in reg.tags) {
val otel = t.declaredOtelName ?: continue
val scoped = t.otelDirection
val result =
when {
scoped == null -> otelNameC(t.identity)

// A tag declared per direction exists only on spans of its direction, so emitting it
// needs none; only resolving the name to it does.
t.sharedNameDirection != null -> "\"${escape(otel)}\""

else -> "direction == KnownTagCodec.DIRECTION_${scoped.name} ? \"${escape(otel)}\" : null"
}
appendLine("case ${serialC(t.identity)}:")
appendLine(" return $result;")
}
appendLine(
"""
Expand All @@ -214,10 +253,34 @@ object KnownTagsEmitter {
}

@Override
public long keyOf(String name) {
public long lookup(String name) {
int slot = StringIndex.EmbeddingSupport.indexOf(KEYOF_HASHES, KEYOF_KEYS, name);
return slot < 0 ? 0L : KEYOF_IDS[slot];
}

@Override
public long directionalKeyOf(String name, int direction) {
switch (name) {
""".trimIndent()
)
for ((name, byDirection) in directional) {
appendLine(" case \"${escape(name)}\":")
appendLine(" switch (direction) {")
for (d in TagConventions.Direction.entries) {
val tag = byDirection[d] ?: continue
appendLine(" case KnownTagCodec.DIRECTION_${d.name}:")
appendLine(" return ${idC(tag)};")
}
appendLine(" default:")
appendLine(" return KnownTagCodec.NO_TAG_IN_DIRECTION_SENTINEL;")
appendLine(" }")
}
appendLine(
"""
default:
return 0L;
}
}
};

private $className() {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ class TagConventions private constructor(
private val spanTypes: Map<String, SpanType>,
private val mixins: Map<String, Mixin>,
private val traceLevel: List<Tag>,
/** Datadog names the tracer intercepts on the set-path, from the tracer overlay. */
val interceptedNames: Set<String> = emptySet(),
) {
/**
* A tag's identity: its Datadog name, plus the direction when that name is declared once per
Expand Down Expand Up @@ -241,6 +243,16 @@ class TagConventions private constructor(
/** The type's own or nearest inherited `span-kind` direction, or null when none is declared. */
private fun directionOf(spanTypes: Map<String, SpanType>, st: SpanType): Direction? = chainOf(spanTypes, st.name).firstNotNullOfOrNull { it.direction }

/** The tracer overlay's `tags`, as a mixin that applies to no span type; empty when none. */
private fun overlayMixin(overlay: Map<String, Any?>): Map<String, Mixin> {
val tags = tagList(overlay["tags"])
if (tags.isEmpty()) return emptyMap()
return mapOf(
OVERLAY_MIXIN to
Mixin(name = OVERLAY_MIXIN, appliesAll = false, appliesTo = emptySet(), tags = tags, refs = emptyList())
)
}

/** Reads `span-kind` as the direction it sets, or null when absent. */
private fun parseDirection(m: Map<String, Any?>, owner: String): Direction? {
val spanKind = m["span-kind"]
Expand All @@ -250,11 +262,31 @@ class TagConventions private constructor(
return (spanKind as String?)?.let { SPAN_KIND_DIRECTIONS.getValue(it) }
}

/** The synthetic mixin holding the tracer overlay's tags; it applies to no span type. */
const val OVERLAY_MIXIN = "tracer overlay"

/**
* Parses [root], the language-agnostic conventions, plus [overlay], this tracer's own set-path
* routing: `tags` declares keys that exist only to be routed (e.g. `resource.name`), so each is
* intercepted, and `intercepted` adds the tags declared in [root] that the tracer also routes.
*/
@Suppress("UNCHECKED_CAST")
fun parse(root: Map<String, Any?>): TagConventions {
fun parse(root: Map<String, Any?>, overlay: Map<String, Any?> = emptyMap()): TagConventions {
for (section in listOf("span_types", "mixins", "trace_level")) {
require(root[section] == null || root[section] is Map<*, *>) { "$section must be a mapping" }
}
require(overlay.keys.all { it == "tags" || it == "intercepted" }) {
"the tracer overlay may only declare `tags` and `intercepted`, not ${overlay.keys - setOf("tags", "intercepted")}"
}
require(refList(overlay["tags"]).isEmpty()) { "tracer overlay tags must be declarations, not refs" }
val interceptedRaw = overlay["intercepted"]
require(interceptedRaw == null || (interceptedRaw is List<*> && interceptedRaw.all { it is String })) {
"intercepted must be a list of Datadog tag names"
}
// An overlay tag exists only to be routed, so it is intercepted without being listed.
val intercepted =
((interceptedRaw as? List<String>) ?: emptyList()).toSet() +
tagList(overlay["tags"]).map { it.ddName }
val spanTypesRaw = (root["span_types"] as? Map<String, Any?>) ?: emptyMap()
val parsedSpanTypes =
spanTypesRaw.mapValues { (name, v) ->
Expand Down Expand Up @@ -298,7 +330,7 @@ class TagConventions private constructor(
refs = refList(m["tags"]),
direction = parseDirection(m, "mixin '$name'"),
)
}
} + overlayMixin(overlay)

for (spanType in parsedSpanTypes.values) {
for (included in spanType.include) {
Expand Down Expand Up @@ -342,7 +374,12 @@ class TagConventions private constructor(
)
}
validateOtelNameScope(spanTypes, mixins, traceLevel)
return TagConventions(spanTypes, mixins, traceLevel)
val conv = TagConventions(spanTypes, mixins, traceLevel, intercepted)
val declared = conv.allDeclaredTags().map { it.ddName }.toSet()
for (name in intercepted) {
require(name in declared) { "intercepted tag '$name' is not declared" }
}
return conv
}

/**
Expand Down
Loading
Loading