Skip to content

Add the tag registry and map OpenTelemetry tag names through it - #12354

Draft
dougqh wants to merge 5 commits into
masterfrom
dougqh/tag-registry-otel
Draft

Add the tag registry and map OpenTelemetry tag names through it#12354
dougqh wants to merge 5 commits into
masterfrom
dougqh/tag-registry-otel

Conversation

@dougqh

@dougqh dougqh commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

What Does This Do

Extracts the tag registry — generated tag ids plus name resolution — from the dense-store stack, so it can land on its own.

A tag id here is identity only: a globally unique serial plus the trace/span level bit. It says what a tag is, never how it is stored or set. Bits [47-32] are documented and held vacant for the co-occurrence slot that the dense tag store assigns by graph coloring, which lands with that store. Nothing in this PR decides how — or whether — a tag is stored, and TagMap storage behavior is unchanged.

On top of that identity, the registry gives each tag a per-namespace name, and OTLP starts using it:

  • keyOf is many→one — a Datadog name or an OpenTelemetry name both resolve to the one id.
  • datadogNameOf / openTelemetryNameOf take the name back out per namespace. nameOf still returns the Datadog name; outbound is namespace-specific, not normalized.
  • openTelemetryTagOf is the one place the pass-through policy lives (declared rename, else the Datadog name), so no serializer re-decides it.
  • OtlpTraceProto renders each known tag under its OpenTelemetry rename when it declares one (http.methodhttp.request.method, http.useragentuser_agent.original, …), falling back to the Datadog name otherwise.

otel-name is optional in the conventions: absent means pass-through under the Datadog name (the RFC "retain" default), a value renames. Because pass-through is the default, every known tag today has an OpenTelemetry name — so there is deliberately no OTel-applicability bit in the id; it would be constant. It returns once a Datadog-only tag exists.

Commits

  1. Add the tag registry — the buildSrc generator (tag-conventions.yaml → committed src/generated/KnownTags.java), KnownTagCodec, and the verifyKnownTags freshness gate wired into check.
  2. Give TagMap.EntryReader its tag id and OpenTelemetry nametagId() and openTelemetryTag() layered on it.
  3. Emit OTLP attributes under OpenTelemetry tag names.
  4. Cover the tag registry — resolution, namespaces, and id partitioning.

Rebased onto current master. Three corrective commits — dropping the Entry.tagId() memo, removing intercept modelling, and linking KnownTagCodec to its generated half — have been squashed into the four above, so each commit reads as its end state. The three sections below record why, since the reasoning is no longer visible in the history.

Why Entry.tagId() is not memoized

TagMap$Entry is the tracer's largest allocation source, one per tag per span on the app thread, and it packs to 40 bytes with only 3 bytes of padding — so a memo field widens every entry to 48 and adds a putfield per construction. The only caller is serialization: background thread, once per entry, and keyOf is a single open-addressed probe over a static final table keyed on an already-cached String hash. Paying it there beats widening every entry. (The dense store will want an id-carrying set(long, String, Object) overload on the reused flyweight instead, which is free — every Entry that survives dense storage is a custom tag, so its tagId() is 0 and a memo's hit rate would be exactly zero.)

Why intercept modelling is gone

The registry originally mirrored TagInterceptor.needsIntercept as an id bit plus a serial-range tier. Codex found four places where the mirror had already drifted from the switch that is the actual authority, and nothing consumed the bit at runtime — it was unused speculative surface whose only effect was to go stale. An id says what a tag is, not how it is set; interception is a TagInterceptor property. The classification returns with the work that consumes it (the id→handler dispatch table that retires TagInterceptor), and re-adding a bit then is purely additive.

This also removed the need for a second conventions file: tag-conventions.java.yaml existed only to carry the Java-specific intercepted: list, so the registry is now one language-agnostic YAML.

Why there is no resolver registration

KnownTagCodec (hand-written: bit layout, naming policy) and generated KnownTags (the name↔id tables) are two halves of one class. Java has no partial classes, so the halves were originally joined at runtime: KnownTags registered its resolver in a static initializer, and the codec captured whatever had registered by the time it first resolved a name.

That left a window. Installed.RESOLVER is static final, so a read before registration latched the empty fallback permanently — every tag unknown for the life of the JVM, with no exception and no log line. Closing it took a CoreTracer poke of KnownTags.init(), and any class resolving a name in its own initializer needed a private lazy holder to dodge it (OtlpTraceProto.Keys, pinned by a forked test). A per-site guard against a process-global hazard, with silent failure for anyone who forgot.

Since KnownTags is generated into the same module and package on the main compile path, Installed can just name KnownTags.RESOLVER — javac resolves the link, and reading a name is what initializes the registry. The window has zero width, so init(), the CoreTracer coupling, the Keys holder and its forked test, the empty fallback codec, register() and isActive() all go away.

The nested holder stays, and now earns its keep for a different reason: KnownTags calls back into KnownTagCodec.serialNum, so were RESOLVER a field of the codec itself, the codec's own initializer would re-enter on the same thread and silently read defaults. One class down means the codec is fully initialized before KnownTags starts. With it stays the read-side payoff — static final of an initialized class, constant-folded, exact klass, so the resolver's switch devirtualizes and inlines outright.

The trade: KnownTagCodec no longer compiles without generated KnownTags, so verifyKnownTags becomes load-bearing rather than a convenience. A build failure beats a silent runtime degradation. Resolver pluggability for tests goes with it — it had no users.

Motivation

PR #12230 maps OpenTelemetry tag names via the registry, and another team is waiting on it. It was based on #12047, which sits on top of the dense store (#12045) and the colored-slot encoding (#12046) — so the OTel work was blocked behind the whole storage stack.

That dependency was real rather than incidental: #12047's generator produces ids in the slot-encoded format #12046 introduces. But the split is clean — TagRegistry.build() already separated serial assignment and OTel-name validation (which never touch slot) from the graph coloring. This PR is the identity-and-names half, targeting master directly.

Additional Notes

Contributor Checklist

  • Format code with ./gradlew spotlessApply
  • :internal-api:test, :internal-api:spotbugsMain, :internal-api:spotlessJavaCheck, :internal-api:verifyKnownTags all green
  • :dd-trace-core:test --tests '*Otlp*', :dd-trace-core:spotlessJavaCheck, :dd-trace-core:forkedTest green
  • Two TracerConnectionReliabilityTest failures were checked against a clean origin/master worktree and reproduce there — pre-existing, unrelated to this change

Jira ticket

N/A

🤖 Generated with Claude Code

@dougqh dougqh added comp: core Tracer core tag: ai generated Largely based on code generated by an AI or LLM tag: no release notes Changes to exclude from release notes type: refactoring labels Aug 31, 2026
@dougqh

dougqh commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a98850bf24

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread tag-conventions.yaml
extends: base
tags:
- { dd-name: http.method, type: string, required: required, otel-name: http.request.method }
- { dd-name: http.status_code, type: int, required: conditional, otel-name: http.response.status_code }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Apply the status-code rename outside the tag map

For normal HTTP spans, http.status_code is intercepted into Metadata.httpStatusCode rather than retained as a TagMap.EntryReader, and MetaWriter.accept writes that field directly using the fixed HTTP_STATUS key. Consequently this declared rename is never consulted and OTLP still emits http.status_code instead of http.response.status_code for essentially every span with a status; the adjusted test masks this by accepting either name. Apply the registry mapping to the first-class metadata emission as well.

Useful? React with 👍 / 👎.

Comment on lines +213 to +214
String otelName = tagEntry.openTelemetryName();
String key = otelName != null ? otelName : tagEntry.tag();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Deduplicate aliases before emitting OTLP attributes

When a span contains both a Datadog key and its OTel alias—for example, auto-instrumentation sets http.method while application OTel code sets http.request.method—both remain distinct entries in TagMap, but this projection writes both as http.request.method. The resulting OTLP attribute list has duplicate keys, so downstream map conversion selects one value according to iteration order. Canonicalize or deduplicate by tag ID before serialization and define which value wins.

Useful? React with 👍 / 👎.

* "not yet resolved" (0L is a valid result -- unknown tag / inactive codec -- so it cannot be
* the sentinel).
*/
long lazyTagId = TAG_ID_NOT_COMPUTED;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep the tag-ID cache off every tag entry

perf: Every span tag is represented by an Entry, so this new long adds an unconditional eight-byte payload per tag even when OTLP export is disabled or tagId() is never called; entries are retained with spans until trace flushing, multiplying the heap and GC cost for high-tag-count or long traces. Keep this exporter-specific cache outside the ubiquitous entry object, or verify the tradeoff with JOL plus an allocation/retained-heap benchmark before accepting it.

AGENTS.md reference: AGENTS.md:L78-L78

Useful? React with 👍 / 👎.

@dougqh dougqh Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Currently, that's true, but I want to expose an API that allows each serializer to use the appropriate namespace easy in the future. And tagRegistry / tagId will be used to drive other changes in future PRs, too.

Comment thread tag-conventions.java.yaml Outdated
Comment on lines +31 to +35
- { dd-name: origin, kind: structural, field: origin } # trace-level field
- { dd-name: sampling.priority, kind: directive }
- { dd-name: manual.keep, kind: directive }
- { dd-name: manual.drop, kind: directive }
- { dd-name: measured, kind: directive }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Register the actual origin and measured keys

The live keys intercepted by TagInterceptor are DDTags.ORIGIN_KEY (_dd.origin) and DDTags.MEASURED (_dd.measured), but these reserved rows register origin and measured instead. As a result, KnownTagCodec.keyOf returns zero for the keys the tracer actually uses while assigning reserved IDs to names that never reach these handlers, so consumers cannot use the new registry to classify those structural/directive tags. Use the existing constant values as the canonical names or explicitly register them as aliases.

Useful? React with 👍 / 👎.

Comment on lines +103 to +108
/** Full stored-tag universe (concrete span types' resolves + trace-level), de-duped by name. */
fun allStoredTags(): List<Tag> {
val union = LinkedHashMap<String, Tag>()
for (type in concreteTypes()) for (t in resolve(type)) union.putIfAbsent(t.name, t)
for (t in traceLevel) union.putIfAbsent(t.name, t)
return union.values.toList()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include declaration-only mixin tags in the registry

allStoredTags() constructs the registry only from resolved concrete span types, but the new ci_visibility mixin applies to test, which the YAML explicitly says is not modeled yet. Consequently its actively used test.name, test.suite, test.status, and test.framework declarations are absent from KnownTags and tag-assignment.txt, and keyOf treats them as unknown. Include mixin declarations in the identity-only registry independently of layout resolution, or model the test span type before generating it.

Useful? React with 👍 / 👎.

@datadog-prod-us1-5

datadog-prod-us1-5 Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

🎯 Code Coverage (details)
Patch Coverage: 69.00%
Overall Coverage: 46.81% (-12.21%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 8d60761 | Docs | View more details | Give us feedback!

@dd-octo-sts

dd-octo-sts Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

Suite Status
Startup 🟢 pass

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 13.96 s 13.95 s [-0.7%; +0.8%] (no difference)
startup:insecure-bank:tracing:Agent 12.91 s 12.98 s [-1.3%; +0.2%] (no difference)
startup:petclinic:appsec:Agent 17.05 s 16.80 s [+0.8%; +2.2%] (maybe worse)
startup:petclinic:iast:Agent 16.94 s 17.04 s [-1.4%; +0.3%] (no difference)
startup:petclinic:profiling:Agent 16.79 s 16.98 s [-2.3%; +0.1%] (no difference)
startup:petclinic:sca:Agent 17.03 s 16.82 s [+0.2%; +2.3%] (maybe worse)
startup:petclinic:tracing:Agent 16.10 s 16.06 s [-0.6%; +1.2%] (no difference)

Commit: 9e149171 · CI Pipeline · Benchmarking Platform UI


Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion.

@dougqh
dougqh force-pushed the dougqh/tag-registry-otel branch 2 times, most recently from 6d5eb31 to d712502 Compare August 31, 2026 21:12
* tracer — and a tracer registers the resolver at init. Being {@code static final} on an
* initialized class, they still constant-fold on the write path.
*/
private static final class Keys {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not sure how, I feel about this.
Extracting into a helper class that can be lazily initialized separately.
In part, this is hiding some of the fun of wiring KnownTags into KnownTagCodec.


// Register the KnownTagCodec resolver so tag-id name resolution (keyOf/nameOf, and the
// OpenTelemetry name mapping the OTLP writer reads) is live for the life of the tracer.
KnownTags.init();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't particularly like this bit of initialization coupling.
However, I did want to keep the KnownTagsCodec.Provider pluggable for testing purposes.

@dougqh

dougqh commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d712502ee8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread tag-conventions.java.yaml Outdated
- { dd-name: manual.keep, kind: directive }
- { dd-name: manual.drop, kind: directive }
- { dd-name: measured, kind: directive }
- { dd-name: analytics.sample_rate, kind: directive } # legacy

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Register the live analytics sample-rate key

When analytics sampling is set through the tracer API, TagInterceptor matches DDTags.ANALYTICS_SAMPLE_RATE, whose value is _dd1.sr.eausr; the repository has no handler for analytics.sample_rate. Consequently KnownTagCodec.keyOf("_dd1.sr.eausr") returns zero, while the generated reserved ID belongs to a key that is stored as an ordinary tag rather than intercepted. Use the live constant value as the reserved name or register it as an alias.

Useful? React with 👍 / 👎.

Comment thread tag-conventions.java.yaml Outdated
# TagInterceptor chain.
# kind: structural -> sets a span/trace field (`field:` names it)
# kind: directive -> triggers sampling/trace behavior
reserved:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add the remaining intercepted directives to the registry

The reserved registry omits asm.keep, ai_guard.keep, _dd.p.ts, and _dd.p.debug, although TagInterceptor.needsIntercept and interceptTag actively consume all four instead of storing them. A repo-wide search finds no declarations for them in either conventions file, so KnownTagCodec.keyOf reports these live AppSec, AI Guard, and debugger directives as unknown and any ID-based handler built on the new registry cannot reproduce the current interceptor behavior.

Useful? React with 👍 / 👎.

Comment thread tag-conventions.java.yaml Outdated
- http.method
- http.url
- servlet.context
- db.statement

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Classify db.statement as a consumed structural key

For every db.statement set through the normal span API, TagInterceptor.interceptDbStatement returns true, so the value is converted into the resource name and never stored in TagMap. Declaring it in this stored-tag list nevertheless generates an ID for which KnownTagCodec.isStored is true, contradicting that API's documented meaning and causing any registry-driven storage routing to retain raw SQL instead of preserving the current consume-only behavior. Model this key as reserved structural data rather than intercepted-but-stored.

Useful? React with 👍 / 👎.

@dougqh
dougqh force-pushed the dougqh/tag-registry-otel branch from 80011c9 to 6439499 Compare September 2, 2026 13:14
@dougqh

dougqh commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9e1491718c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// supplying its own key for a custom tag the registry does not name. This is the straight
// rename projection only: suppressing a Datadog-only tag from OpenTelemetry, per-exporter
// opt-in, and additional namespaces are deferred to the OpenTelemetry follow-on.
String key = tagEntry.openTelemetryTag();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Apply registry renames to the JSON trace encoder

When OtlpConfig.Protocol.HTTP_JSON is selected, OtlpWriter uses OtlpTraceJsonCollector, whose OtlpTraceJson.writeSpanTag still emits tagEntry.tag() and whose metadata writer still uses the fixed HTTP_STATUS key. Although this protobuf path now applies the registry projection, JSON exports therefore continue to emit Datadog names such as http.method and http.status_code instead of http.request.method and http.response.status_code; apply the same per-entry and intercepted-metadata mapping to the JSON encoder.

Useful? React with 👍 / 👎.

private fun tagList(tags: Any?): List<Tag> =
(tags as? List<Map<String, Any?>>)?.map { m ->
Tag(
name = m["dd-name"].toString(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject tag declarations without dd-name

If a future YAML declaration omits or misspells dd-name, Kotlin's nullable toString() converts the missing value into the literal tag name "null". Generation and the freshness check then succeed with a public NULL_* registry entry while the intended tag is silently absent, so a simple conventions typo can ship a corrupted registry instead of failing CI; require a nonblank string here as is already done for otel-name.

Useful? React with 👍 / 👎.

writeSpanTag(buf, THREAD_NAME, metadata.getThreadName());
if (metadata.getHttpStatusCode() != null) {
writeSpanTag(buf, HTTP_STATUS, metadata.getHttpStatusCode());
writeSpanTag(buf, HTTP_STATUS_CODE_KEY, metadata.getHttpStatusCode());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Emit the renamed HTTP status as an integer attribute

For the normal setHttpStatusCode path, Metadata.getHttpStatusCode() is a UTF8BytesString, so this call selects the CharSequence overload and writes http.response.status_code as an OTLP string_value. The conventions declare this renamed attribute as int, and downstream OTel consumers expecting the semantic-convention integer will see the wrong type for essentially every auto-instrumented HTTP status; pass the numeric status to the long-attribute overload instead.

Useful? React with 👍 / 👎.

dougqh and others added 5 commits September 2, 2026 12:57
Introduces a language-agnostic tag registry. tag-conventions.yaml
declares each known tag once; a buildSrc Gradle plugin generates
KnownTags.java from it into internal-api/src/generated, committed and on
the main compile path, with a verifyKnownTags task wired into check so a
stale checkout fails the build rather than drifting.

A tag id is IDENTITY, not storage: a globally unique 16-bit serial in
bits [63-48] plus the trace/span level bit. It says what a tag is, never
how it is stored or set. Bits [47-32] are documented and held vacant for
the co-occurrence slot the dense tag store assigns by graph coloring,
so that store lands as a purely additive change.

KnownTagCodec is the hand-written half of the registry -- bit layout and
naming policy -- and generated KnownTags is the data half. Java has no
partial classes, so the codec's Installed holder names KnownTags.RESOLVER
directly: reading a tag name is what initializes the registry, with no
registration call and no ordering to get wrong. The holder keeps the
resolver in a static final of an initialized class, which the JIT
constant-folds to an exact klass, so keyOf/nameOf devirtualize and inline
with no lock, volatile read, null check or virtual call. The nesting is
load-bearing: KnownTags calls back into the codec, so a field on the
codec itself would re-enter a half-initialized class.

keyOf is many-to-one -- a Datadog name or an OpenTelemetry name both
resolve to the one id -- and datadogNameOf/openTelemetryNameOf take the
name back out per namespace. openTelemetryTagOf is the single home for
the pass-through policy (declared rename, else the Datadog name), so no
serializer re-decides it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tagId() resolves the entry's key through the registry, and
openTelemetryTag() layers the namespace projection on top: the tag's
declared OpenTelemetry rename when it has one, otherwise its own key.
The reader supplies that last fallback because a custom tag has no
registry name, and only the holder of the key can name it.

Entry.tagId() is deliberately computed rather than memoized, unlike
hash(). TagMap$Entry is the tracer's largest allocation source -- one per
tag per span, on the app thread -- and it packs to 40 bytes with only 3
bytes of padding, so a long memo widens every entry to 48 and adds a
putfield per construction. The only caller is serialization: background
thread, once per entry, where keyOf is a single open-addressed probe over
a static final table keyed on an already-cached String hash.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
OTLP is the OpenTelemetry wire format, so each known tag is rendered
under its OpenTelemetry name: http.method as http.request.method,
http.useragent as user_agent.original, and so on, falling back to the
Datadog name for a tag that declares no rename and to the entry's own key
for a custom tag the registry does not know.

Tags the tracer intercepts into first-class Metadata fields never reach
the per-entry projection, so their keys resolve through the same registry
policy once each at class init -- otherwise service and http.status_code
would keep emitting under Datadog names while every other tag moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
KnownTagsTest pins the registry itself: keyOf/nameOf round-trip for every
known tag, many-to-one resolution from either namespace, serial encoding
and the trace-level bit, and unknown names resolving to 0.
TagMapNamespaceNamesTest covers EntryReader.openTelemetryTag() -- rename,
pass-through, and the custom-tag fallback the codec cannot supply.
OtlpTraceProtoTest asserts attributes now land under OpenTelemetry names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The registry was built by resolving concrete span types, so a tag reached
the union only through a type that could carry it. A mixin attaches via
its applies: targets, and ci_visibility applies to `test`, which the
conventions do not model yet -- so test.name, test.suite, test.status and
test.framework were dropped, silently. keyOf reported live CI Visibility
tags as unknown, and nothing in the generated output showed the four
declarations had gone anywhere.

An id is identity, and identity does not depend on layout: a declared tag
gets an id whether or not a modeled span type can carry it today. So the
registry is now built from declarationGroups() -- trace-level, span types
and every mixin -- which is also what makes mixins behave uniformly
rather than working only when their target happens to be modeled.

The layout gap is real, just not fatal, so the generator reports it:
resolved-tags.txt names each mixin whose applies: target is unmodeled and
says its tags are registered but occupy no per-type slot. Declaring tags
ahead of the span type that will carry them is a legitimate intermediate
state; being unable to see it is not.

Registry grows 47 -> 51 tags. Serials are assigned over the sorted name
list, so the four insertions shift the ids of tags sorting after them --
fine, since ids are in-process with no cross-release stability.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dougqh
dougqh force-pushed the dougqh/tag-registry-otel branch from 9e14917 to 8d60761 Compare September 2, 2026 18:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: core Tracer core tag: ai generated Largely based on code generated by an AI or LLM tag: no release notes Changes to exclude from release notes type: refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant