Skip to content

Set known tags by id on TagMap and spans (otlp - tag registry - phase 2) - #12715

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 6 commits into
masterfrom
dougqh/tag-id-setters
Oct 13, 2026
Merged

gh-worker-dd-mergequeue-cf854d[bot] merged 6 commits into
masterfrom
dougqh/tag-id-setters

Conversation

@dougqh

@dougqh dougqh commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

What Does This Do

Adds setting a known tag by its id to TagMap and the span API, on top of the tag registry from #12354 (stacked on it; the diff here is only this PR).

  • TagMap.set(long tagId, value) for Object, CharSequence, and the primitives. The entry is built under the tag's canonical name from KnownTagCodec.nameOf, skipping the canonicalizing name lookup the String setters pay on every new entry. An id that names no known tag is rejected.
  • TagMap.Entry.create(long tagId, value), the public way to build an entry ahead of time (as decorators do for cached entries), at the same layer as AgentSpan.setTag. It has the same contract as the name-keyed create: a null or empty value yields no entry.
  • AgentSpan.setTag(long tagId, value), as default methods that resolve the id to its name and delegate to the String setter, so every AgentSpan implementation stays correct with no changes.
  • DDSpan / DDSpanContext overrides with the same behavior as the String family: TagInterceptor still runs on the tag's name (it is name-keyed), primitives still box only when the tag might be intercepted, and the http.status_code quirk is kept. The difference is that a stored tag is set by id.
  • TagMap hashes entries by tag id. TagMap.Entry holds a 64-bit tag hash, computed once at construction: a known tag's id, or a custom tag's name hash in the low 32 bits. An id's serial sits in bits 63-48, so the two can never collide.
    • tagId() becomes a field read instead of a keyOf lookup.
    • Name lookups (getEntry, getAndRemove) take one keyOf, instead of canonicalizing and then hashing the name separately.
    • Buckets fold the hash from bit 48, so known tags spread by serial. A custom tag's bucket hash is unchanged.
    • BucketGroup keeps its int hashes, so only Entry grows: int to long costs 8 bytes per entry because of alignment. That is an accepted temporary cost until the dense store.
  • First caller: BaseDecorator.onPeerConnection / setPeerPort, which run on every client span, now set peer.hostname, peer.ipv4/ipv6 and peer.port by KnownTags id.

Motivation

Under the OTLP <-> Datadog mapping, an ID identifies one tag exactly, where a name may not. #12713 is making the tag registry direction-aware, and there peer.port becomes two tags (the server's port on outbound spans, the client's on inbound ones) that share a Datadog name. Writers then need to set the specific tag, which is what an id expresses. This PR provides that API, independent of the direction work:

            ┌──► #12713  (registry: span directions)  ──┐
#12354 ─────┤                                           ├──► next: writers resolve to ids by direction
            └──► this PR (set tags by id)  ─────────────┘

It follows the API shape of #11901, but implemented on the current TagMap rather than the dense store, which #11901 is stacked on. The dense store can later swap the implementation behind the same signatures.

Additional Notes

Contributor Checklist

  • :internal-api:test (TagMap* incl. TagMapFuzzTest, 384 tests; KnownTags*; new TagMapSetByIdTest, TagMapTagHashTest, TagMapEntryCreateByIdTest), :internal-api:spotlessCheck

  • :dd-trace-core:test (DDSpan* incl. new DDSpanSetTagByIdTest, *taginterceptor*, *otlp*, common.writer.*, common.metrics.*; 1279 tests), :dd-trace-core:spotlessCheck. DDAgentWriterCombinedTest.unixSocketTimeoutKeepsWorkerAliveAndReconnects failed once and passed on rerun here and on the base branch; it's a timing test.

  • New TagMapInsertBenchmark, before vs. after hashing entries by tag id. MacBook, JDK 21, default flags, @Fork(2) triage; 12 tags per op:

    Benchmark before ns/op after ns/op change B/op
    knownByName 130.9 ± 1.8 121.1 ± 3.8 −7.5% 688 → 736
    knownById 100.7 ± 5.5 94.7 ± 1.7 −5.9% 688 → 736
    customByName 82.2 ± 2.7 80.6 ± 2.4 −1.9% (noise) 640 → 736

    The first run caught a regression: knownById was +17%, because the id constructor resolved the name twice. That is fixed in 62f1026. Known tags net only +48 B because spreading them by serial removed a bucket collision.

  • :dd-java-agent:agent-bootstrap:test (*Decorator*, 413 tests), :dd-java-agent:agent-bootstrap:spotlessCheck

Jira ticket

N/A

🤖 Generated with Claude Code

@dougqh dougqh added type: feature Enhancements and improvements comp: core Tracer core tag: no release notes Changes to exclude from release notes tag: ai generated Largely based on code generated by an AI or LLM labels Oct 1, 2026
@datadog-official

This comment has been minimized.

@dd-octo-sts

dd-octo-sts Bot commented Oct 1, 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 14.72 s 14.57 s [+0.2%; +1.8%] (maybe worse)
startup:insecure-bank:tracing:Agent 13.60 s 13.60 s [-0.7%; +0.6%] (no difference)
startup:petclinic:appsec:Agent 17.25 s 17.08 s [+0.1%; +1.9%] (maybe worse)
startup:petclinic:iast:Agent 16.81 s 17.05 s [-2.2%; -0.6%] (maybe better)
startup:petclinic:profiling:Agent 16.66 s 16.97 s [-2.8%; -0.9%] (maybe better)
startup:petclinic:sca:Agent 17.17 s 16.88 s [+0.9%; +2.5%] (maybe worse)
startup:petclinic:tracing:Agent 16.16 s 16.05 s [-0.2%; +1.5%] (no difference)

Commit: 8125e018 · 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 marked this pull request as ready for review October 1, 2026 19:37
@dougqh
dougqh requested review from a team as code owners October 1, 2026 19:37
@dougqh
dougqh requested review from bric3 and mcculls and removed request for a team October 1, 2026 19:37

@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: f38b1f88fe

ℹ️ 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 internal-api/src/main/java/datadog/trace/api/TagMap.java Outdated

@datadog-official datadog-official Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bits Code Review: FAIL

The new hash fold maps _dd.djm.enabled to the reserved zero sentinel, allowing bucket collisions to lose the tag or cause lookup failures.

Open Bits AI session

🤖 Bits Code Review · Commit f38b1f8 · @DataDog review to ask questions

Comment thread internal-api/src/main/java/datadog/trace/api/TagMap.java Outdated
@dougqh dougqh changed the title Set known tags by id on TagMap and spans Set known tags by id on TagMap and spans (otlp - tag registry - phase 2) Oct 2, 2026
Comment thread dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java Outdated
Comment thread internal-api/src/main/java/datadog/trace/api/TagMap.java Outdated
dougqh added a commit that referenced this pull request Oct 7, 2026
Builds on the registry directions (#12713) and id-keyed setters
(#12715, included here): KnownTagCodec resolves names by direction,
TagMap finds a shared name's tag whichever direction it holds, and
DDSpanContext resolves direction-dependent names by the span's kind.
Squashed from the review history of #12731.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dougqh
dougqh force-pushed the dougqh/tag-registry-otel branch from c3c341e to e49a183 Compare October 7, 2026 12:50
dougqh added a commit that referenced this pull request Oct 7, 2026
Adds id-keyed setters, getters and removal to TagMap, TagMap.Entry and
spans, so a writer that knows the tag skips the name lookup. Squashed
from the review history of #12715.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dougqh
dougqh force-pushed the dougqh/tag-id-setters branch from c947ea5 to 55ab351 Compare October 7, 2026 12:50
@dougqh
dougqh force-pushed the dougqh/tag-registry-otel branch from e49a183 to 44e132c Compare October 7, 2026 13:11
@dougqh
dougqh requested a review from a team as a code owner October 7, 2026 13:11
@dougqh
dougqh removed the request for review from a team October 7, 2026 13:11
@dougqh
dougqh force-pushed the dougqh/tag-id-setters branch from b569cd2 to 3ab5862 Compare October 9, 2026 12:37
dougqh and others added 3 commits October 9, 2026 10:01
span.setTag(0L, "") or a null value reached TagMap.getAndRemove(long),
which rejects unknown ids, so clearing threw IllegalArgumentException
while every id-keyed setter ignores an unknown id. removeTag(long) now
ignores it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The long-valued setters prechecked with needsIntercept, but when the
interceptor declined the tag they discarded the box it was given and stored
the primitive. Follow the same precheckIntercept -> setBox shape as the
other primitive setters, so TagMap keeps the box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
peer.port is declared per direction in the registry (#12713), so it has no
direction-free id: BaseDecorator, shared by client and server decorators,
sets it by name, and the set-by-id tests use an unshared int tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dougqh added a commit that referenced this pull request Oct 10, 2026
Builds on the registry directions (#12713) and id-keyed setters
(#12715, included here): KnownTagCodec resolves names by direction,
TagMap finds a shared name's tag whichever direction it holds, and
DDSpanContext resolves direction-dependent names by the span's kind.
Squashed from the review history of #12731.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dougqh
dougqh requested a review from a team as a code owner October 13, 2026 15:00
@dougqh
dougqh requested review from rachelyangdog and removed request for a team October 13, 2026 15:00
@dougqh
dougqh enabled auto-merge October 13, 2026 15:09

@datadog-official datadog-official Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bits Code Review: FAIL

ID-keyed peer.port entries lose their identity in read-through TagMaps: removing one direction hides the other, and replacing an inherited entry reports no previous value.

Open Bits AI session

🤖 Bits Code Review · Commit de00517

Comment thread internal-api/src/main/java/datadog/trace/api/TagMap.java
Comment thread internal-api/src/main/java/datadog/trace/api/TagMap.java
When nothing local is replaced, getAndSet looks the tag up in the
read-through parent to report its prior value. It did that by name. A
shared Datadog name (peer.port) resolves to no tag without a direction,
so setting PEER_PORT_OUTBOUND_ID on a child of a parent holding it
reported no prior value. The parent is now searched by the entry's own
tag and hash, which also saves a name lookup for every other tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dougqh

dougqh commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

@bric3 a friendly ping: this one still needs a lang-platform approval. mcculls and vandonr have approved; the latest bot findings are addressed (one fixed in 8125e01 with tests, the other answered); and the branch is current with master.

@dougqh
dougqh added this pull request to the merge queue Oct 13, 2026
@dd-octo-sts

dd-octo-sts Bot commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-10-13 21:09:31 UTC ℹ️ Start processing command /merge


2026-10-13 21:09:37 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in master is approximately 1h (p90).


2026-10-13 22:39:21 UTC ℹ️ MergeQueue: This merge request was merged

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 13, 2026
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot merged commit e5664c8 into master Oct 13, 2026
610 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot deleted the dougqh/tag-id-setters branch October 13, 2026 22:39
@github-actions github-actions Bot added this to the 1.68.0 milestone Oct 13, 2026
dougqh added a commit that referenced this pull request Oct 14, 2026
Master now has #12715 (set-by-id, squashed) and #12037 (decorator
SpanPrototype). Conflicts resolve to this branch's newer id-keyed code,
plus:
- getAndSet finds its inherited prior by the entry's id (#12715's last fix);
  the shared-name test for it is dropped, since peer.port now has one id.
- #12037's resolveSplitByTagsPrecedence checks component by id and language
  by name against the serial-indexed split-by-tags table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dougqh added a commit that referenced this pull request Oct 14, 2026
Brings master, including #12715 (now merged) and Spotless 8.10.4, plus
#12817's new tests and its tag-registry formatting fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dougqh added a commit that referenced this pull request Oct 14, 2026
Brings master (#12715 merged, Spotless 8.10.4), #12817, and #12731's
direction-neutral peer.port id. The dense store moves onto that model:

- peer.port is one tag with one id, so the undirected id (e7a8721),
  the UNDIRECTED and SHARED_NAME bits, and the dense store's in-place
  relabel are gone. Bit 0 is #12731's HAS_SCOPED_ALIAS.
- Slot coloring is re-applied on #12731's one-tag-per-name registry:
  a name declared per direction is one vertex.
- Tests and benchmarks use PEER_PORT_ID; the undirected-model tests are
  dropped in favor of #12731's SpanTags and directional span tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gh-worker-dd-mergequeue-cf854d Bot pushed a commit that referenced this pull request Oct 15, 2026
…- phase 3) (#12817)

Set known tags by id on TagMap and spans

Adds id-keyed setters, getters and removal to TagMap, TagMap.Entry and
spans, so a writer that knows the tag skips the name lookup. Squashed
from the review history of #12715.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Ignore unknown ids when clearing a tag by id; fix a comment wrap

span.setTag(0L, "") or a null value reached TagMap.getAndRemove(long),
which rejects unknown ids, so clearing threw IllegalArgumentException
while every id-keyed setter ignores an unknown id. removeTag(long) now
ignores it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Precheck and reuse the box in long-valued setters

The long-valued setters prechecked with needsIntercept, but when the
interceptor declined the tag they discarded the box it was given and stored
the primitive. Follow the same precheckIntercept -> setBox shape as the
other primitive setters, so TagMap keeps the box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Keep peer.port off the id path

peer.port is declared per direction in the registry (#12713), so it has no
direction-free id: BaseDecorator, shared by client and server decorators,
sets it by name, and the set-by-id tests use an unshared int tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge set-by-id (#12715) onto master

Mark intercepted tags with an id bit, via a tracer overlay

Set-path routing is a per-language concern, so it lives in a Java overlay
next to tag-conventions.yaml rather than in the language-agnostic
conventions. The overlay declares the keys that exist only to be routed
(resource.name, error, sampling directives, ...) and lists every tag
TagInterceptor may route. Each listed tag's id carries the INTERCEPTED bit
(bit 1), so a setter called with a constant id can fold the interception
test away. Serial numbers become public so TagInterceptor can switch on
them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Dispatch TagInterceptor on tag ids

TagInterceptor switches on the tag's serial rather than its name, so a
known tag is matched under any of its names and the *_OTEL_NAME cases go
away. needsIntercept(long) tests the INTERCEPTED bit first, which folds
away for a constant id. split-by-tags entries resolve to serials at
construction (each direction for a name declared per direction); only
custom tags are still matched by name. A test checks that exactly the
tags carrying the bit have a case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Set tags on spans by id first

A name setter resolves a known tag's name to its id once and sets it as the
id-keyed setters do; only a custom tag is still handled by name. The
id-keyed setters no longer look up the name to feed the interceptor, so with
a constant id the interception test folds to the INTERCEPTED bit. Entry
paths (builder ledger, prototypes, default tags) route by the entry's id
and precheck before boxing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Look up tag names by serial in an array, not a switch

The generated nameOf switched over every tag's serial: 491 bytes of
bytecode, too big for C2 to inline, so each id-keyed set paid two
out-of-line calls into it (the span's unknown-id guard and the entry's
name). Read a NAMES_BY_SERIAL array instead; nameOf is now 23 bytes and
inlines at every caller.

Also adds SetTagBenchmark (span setTag by constant id, non-constant id,
name, and custom name, against a bare and a synchronized TagMap store).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Point stale comments at the tracer overlay and the intercepted bit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Index the split-by-tags table by serial, sized to every known tag

The table is always allocated and never resized, so the split check -- the
only run-time check left on a non-intercepted constant-id set -- is a single
load, with no null or length test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Check for a known tag id with a range check, not a name lookup

The span's unknown-id guard called nameOf; KnownTagCodec.isKnown answers the
same question from the serial and a generated SERIAL_LIMIT, so it folds
away for a constant id. Also note that serials are not stable across
releases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Pin interceptTag(long) above C2's inlining size

DDSpanContext.setTag calls interceptTag only for intercepted tags. Kept out
of line (over FreqInlineSize, 325 bytes of bytecode), it never brings the
profiled handler bodies into setTag's compiled code, which would push setTag
past InlineSmallCode and stop callers inlining it -- the inlining a constant
id needs to fold its interception test. A test now fails if the switch
shrinks below the limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Tighten comments on the id-keyed interceptor

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Treat overlay tags as intercepted; tidy from review

An overlay tag exists only to be routed, so the generator now marks it
intercepted without a second listing; intercepted: lists only the tags
declared in tag-conventions.yaml that the tracer also routes. Also restore
the doc comment and @Suppress that the overlay helper displaced, read the
serial limit one way, inline isUnknownTag, and drop an unused default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Reject shared-name tag ids on the id-keyed TagMap and span paths

A tag declared once per direction (peer.port) has two ids but one
shared Datadog name, and keyOf resolves that name to neither. An entry
keyed by one of those ids hashed by the id while name-based access
hashed the name as a custom tag, so get/remove/containsKey by name
missed it and a later set by name produced a duplicate key.

Encode a SHARED_NAME flag in the reserved id bit 0 so the check folds
for a constant id. TagMap's id-keyed create/get/remove reject such ids,
and DDSpanContext's id-keyed setters ignore them, as they do unknown
ids, until name resolution knows the span's direction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Test rejection of shared-name tag ids

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Build custom tags by name and known tags by id

Entry's package-private name factories are now custom-only: they skip the
registry and assert the name is not a known tag. Callers that start from a
name resolve it once -- TagMap.set(String, ...) forwards a known tag to
set(long, ...), while getAndSet, put, Ledger.set, putAll and the public
Entry.create(String, ...) API go through anyEntryFor and friends -- so a
custom tag pays one registry lookup instead of two.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge master into id-tag-interceptor (#12817)

# Conflicts:
#	.github/CODEOWNERS

Give the tracer core a restricted, trusted path into TagMap

TagMap.internals() returns a small Internals view whose setKnown(...) skips
the id validation the checked set(long, ...) repeats; DDSpanContext's
id-keyed setters, which have already checked isKeyableById, now use it.
Internals is created per call, so escape analysis removes it.

internals() is marked with a new @restricted(allowedIn = ...) annotation,
and gradle/forbiddenApiFilters/instrumentation.txt bans it in
instrumentation modules, so calling it there fails forbiddenApisMain.
Internals is also where the batched setTagsFrom/TagSink writes will go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Mark TagMap.Internals @NoEscape

It exists to be created, used within the expression, and scalar-replaced;
storing one would force a real allocation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Add datadog.apicontract to CODEOWNERS

Restricted lives in the new datadog.apicontract package, which had no owner.
Give it the same owner as its sibling datadog.perfcontract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge branch 'master' into dougqh/id-tag-interceptor

Give peer.port one direction-neutral id

peer.port was modeled as one tag per direction: PEER_PORT_INBOUND_ID and
PEER_PORT_OUTBOUND_ID, sharing the Datadog name and marked with a
SHARED_NAME id bit. Two ids under one name kept producing lookup and
supersede bugs: keyOf("peer.port") resolved to neither id, so a value set
by name and one set by id landed in different entries, and tombstones and
getAndSet by name could not find what an id had stored.

The generator now merges a name's per-direction declarations into one tag
with one id (PEER_PORT_ID), and only its OpenTelemetry name depends on the
span's direction: openTelemetryNameOf(id, direction) returns client.port
inbound and server.port outbound. The SHARED_NAME bit, hasSharedName and
isKeyableById go away (bit 0 is left unused), and keyOf("peer.port")
returns PEER_PORT_ID.

KnownTagCodec gains the DIRECTION_* constants and
openTelemetryTagOf(id, direction). Direction-scoped OpenTelemetry names
are still not resolved when written by name.

This is the direction-neutral-id part of #12731's redesign; its
direction-scoped aliases and read-time resolution are left to that PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Name peer.port by the span's kind in OTLP export

With peer.port now one direction-neutral tag, its OpenTelemetry name
depends on the span: server.port on a client span, client.port on a
server span. The OTLP JSON and protobuf writers compute the span's
direction once from its span.kind and name each tag for it, so
direction-scoped renames apply too: peer.hostname exports as
server.address on a client span, and network.client.ip as
network.peer.address on a server span.

KnownTagCodec gains directionOf(TagMap) / directionOfKind(String), and
TagMap.EntryReader gains openTelemetryTag(direction), standing in for
the SpanTags helpers #12731 introduces.

Ported from #12731's redesign.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Let C2 inline the constructors of TagMap's nested classes

Entry's full constructor and the Internals and Ledger constructors were
private, so javac (targeting Java 8, without nestmates) gave each one a
synthetic access constructor taking the never-loaded class TagMap$1. C2
refuses to inline a call whose signature names an unloaded class
("unloaded signature classes"). So the Internals view the core creates
for each id write could not be scalar-replaced, and every Entry built
through that constructor went through a real call. Making the
constructors package-private removes the accessors and TagMap$1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Remove TagMap's remaining synthetic accessors and guard against them

Several private members were used across TagMap's nested classes, so
javac (targeting Java 8) generated access$ methods for them: Entry's
*2Prim helpers and requireKnownName, TagMap.parentEntryVisible,
TagMap.putEntry and the TagMap.buckets field. Each accessor adds a level
of inlining depth. putEntry's accessor sits on Internals.setKnown, the
core's trusted id-write path. All of these are now package-private.

TagMapSyntheticAccessorTest fails if TagMap or any of its nested classes
gains a synthetic accessor again. It is the prototype for the
@NoSyntheticAccess contract (APMLP-1976).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge master into id-tag-interceptor

Master now has #12715 (set-by-id, squashed) and #12037 (decorator
SpanPrototype). Conflicts resolve to this branch's newer id-keyed code,
plus:
- getAndSet finds its inherited prior by the entry's id (#12715's last fix);
  the shared-name test for it is dropped, since peer.port now has one id.
- #12037's resolveSplitByTagsPrecedence checks component by id and language
  by name against the serial-indexed split-by-tags table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Cover TagMap.Internals, KnownTagCodec and EntryReader paths

Brings the three classes back over jacoco's thresholds: the trusted
setters' Object/float/double overloads, isIntercepted, isKnown's range
edges, canonicalTagName, span-kind direction, and an entry's directional
OpenTelemetry name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Format the tag-registry generator to its spotless rules

build-logic is a separate Gradle build, so the root spotlessApply never
formats it. Three generator lines were wrapped where its Kotlin formatter
keeps them on one line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Order the tag-id carve bits by position

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge master into id-tag-interceptor

Brings #12814 (split-by-tags by OpenTelemetry name). Id matching
already covers OpenTelemetry names, so this PR's TagInterceptor wins and
#12814's withCanonicalNames goes, as planned. #12814's tests stay; one
expectation changes: with db.type configured, setTag("db.system", ...)
now splits too, since a known tag matches under any of its names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Re-apply split-by-tags language by its id

resolveSplitByTagsPrecedence checked language through the custom-tag
overload, which only matches names that resolve to no known tag.
language is known, so the check was always false and a prototype's
language was never re-applied over another split-by-tags candidate the
prototype set. Check and read it by LANGUAGE_ID, as for component.

Found by Codex, via a review comment from AlexeyKuznetsov-DD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Say what a tag id's intercepted bit does and doesn't decide

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
gh-worker-dd-mergequeue-cf854d Bot pushed a commit that referenced this pull request Oct 20, 2026
…12893)

Set known tags by id on TagMap and spans

Adds id-keyed setters, getters and removal to TagMap, TagMap.Entry and
spans, so a writer that knows the tag skips the name lookup. Squashed
from the review history of #12715.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Ignore unknown ids when clearing a tag by id; fix a comment wrap

span.setTag(0L, "") or a null value reached TagMap.getAndRemove(long),
which rejects unknown ids, so clearing threw IllegalArgumentException
while every id-keyed setter ignores an unknown id. removeTag(long) now
ignores it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Precheck and reuse the box in long-valued setters

The long-valued setters prechecked with needsIntercept, but when the
interceptor declined the tag they discarded the box it was given and stored
the primitive. Follow the same precheckIntercept -> setBox shape as the
other primitive setters, so TagMap keeps the box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Keep peer.port off the id path

peer.port is declared per direction in the registry (#12713), so it has no
direction-free id: BaseDecorator, shared by client and server decorators,
sets it by name, and the set-by-id tests use an unshared int tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge set-by-id (#12715) onto master

Mark intercepted tags with an id bit, via a tracer overlay

Set-path routing is a per-language concern, so it lives in a Java overlay
next to tag-conventions.yaml rather than in the language-agnostic
conventions. The overlay declares the keys that exist only to be routed
(resource.name, error, sampling directives, ...) and lists every tag
TagInterceptor may route. Each listed tag's id carries the INTERCEPTED bit
(bit 1), so a setter called with a constant id can fold the interception
test away. Serial numbers become public so TagInterceptor can switch on
them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Dispatch TagInterceptor on tag ids

TagInterceptor switches on the tag's serial rather than its name, so a
known tag is matched under any of its names and the *_OTEL_NAME cases go
away. needsIntercept(long) tests the INTERCEPTED bit first, which folds
away for a constant id. split-by-tags entries resolve to serials at
construction (each direction for a name declared per direction); only
custom tags are still matched by name. A test checks that exactly the
tags carrying the bit have a case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Set tags on spans by id first

A name setter resolves a known tag's name to its id once and sets it as the
id-keyed setters do; only a custom tag is still handled by name. The
id-keyed setters no longer look up the name to feed the interceptor, so with
a constant id the interception test folds to the INTERCEPTED bit. Entry
paths (builder ledger, prototypes, default tags) route by the entry's id
and precheck before boxing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Look up tag names by serial in an array, not a switch

The generated nameOf switched over every tag's serial: 491 bytes of
bytecode, too big for C2 to inline, so each id-keyed set paid two
out-of-line calls into it (the span's unknown-id guard and the entry's
name). Read a NAMES_BY_SERIAL array instead; nameOf is now 23 bytes and
inlines at every caller.

Also adds SetTagBenchmark (span setTag by constant id, non-constant id,
name, and custom name, against a bare and a synchronized TagMap store).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Point stale comments at the tracer overlay and the intercepted bit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Index the split-by-tags table by serial, sized to every known tag

The table is always allocated and never resized, so the split check -- the
only run-time check left on a non-intercepted constant-id set -- is a single
load, with no null or length test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Check for a known tag id with a range check, not a name lookup

The span's unknown-id guard called nameOf; KnownTagCodec.isKnown answers the
same question from the serial and a generated SERIAL_LIMIT, so it folds
away for a constant id. Also note that serials are not stable across
releases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Pin interceptTag(long) above C2's inlining size

DDSpanContext.setTag calls interceptTag only for intercepted tags. Kept out
of line (over FreqInlineSize, 325 bytes of bytecode), it never brings the
profiled handler bodies into setTag's compiled code, which would push setTag
past InlineSmallCode and stop callers inlining it -- the inlining a constant
id needs to fold its interception test. A test now fails if the switch
shrinks below the limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Tighten comments on the id-keyed interceptor

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Treat overlay tags as intercepted; tidy from review

An overlay tag exists only to be routed, so the generator now marks it
intercepted without a second listing; intercepted: lists only the tags
declared in tag-conventions.yaml that the tracer also routes. Also restore
the doc comment and @Suppress that the overlay helper displaced, read the
serial limit one way, inline isUnknownTag, and drop an unused default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Reject shared-name tag ids on the id-keyed TagMap and span paths

A tag declared once per direction (peer.port) has two ids but one
shared Datadog name, and keyOf resolves that name to neither. An entry
keyed by one of those ids hashed by the id while name-based access
hashed the name as a custom tag, so get/remove/containsKey by name
missed it and a later set by name produced a duplicate key.

Encode a SHARED_NAME flag in the reserved id bit 0 so the check folds
for a constant id. TagMap's id-keyed create/get/remove reject such ids,
and DDSpanContext's id-keyed setters ignore them, as they do unknown
ids, until name resolution knows the span's direction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Test rejection of shared-name tag ids

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Build custom tags by name and known tags by id

Entry's package-private name factories are now custom-only: they skip the
registry and assert the name is not a known tag. Callers that start from a
name resolve it once -- TagMap.set(String, ...) forwards a known tag to
set(long, ...), while getAndSet, put, Ledger.set, putAll and the public
Entry.create(String, ...) API go through anyEntryFor and friends -- so a
custom tag pays one registry lookup instead of two.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge master into id-tag-interceptor (#12817)

# Conflicts:
#	.github/CODEOWNERS

Give the tracer core a restricted, trusted path into TagMap

TagMap.internals() returns a small Internals view whose setKnown(...) skips
the id validation the checked set(long, ...) repeats; DDSpanContext's
id-keyed setters, which have already checked isKeyableById, now use it.
Internals is created per call, so escape analysis removes it.

internals() is marked with a new @restricted(allowedIn = ...) annotation,
and gradle/forbiddenApiFilters/instrumentation.txt bans it in
instrumentation modules, so calling it there fails forbiddenApisMain.
Internals is also where the batched setTagsFrom/TagSink writes will go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Mark TagMap.Internals @NoEscape

It exists to be created, used within the expression, and scalar-replaced;
storing one would force a real allocation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Add datadog.apicontract to CODEOWNERS

Restricted lives in the new datadog.apicontract package, which had no owner.
Give it the same owner as its sibling datadog.perfcontract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge branch 'master' into dougqh/id-tag-interceptor

Give peer.port one direction-neutral id

peer.port was modeled as one tag per direction: PEER_PORT_INBOUND_ID and
PEER_PORT_OUTBOUND_ID, sharing the Datadog name and marked with a
SHARED_NAME id bit. Two ids under one name kept producing lookup and
supersede bugs: keyOf("peer.port") resolved to neither id, so a value set
by name and one set by id landed in different entries, and tombstones and
getAndSet by name could not find what an id had stored.

The generator now merges a name's per-direction declarations into one tag
with one id (PEER_PORT_ID), and only its OpenTelemetry name depends on the
span's direction: openTelemetryNameOf(id, direction) returns client.port
inbound and server.port outbound. The SHARED_NAME bit, hasSharedName and
isKeyableById go away (bit 0 is left unused), and keyOf("peer.port")
returns PEER_PORT_ID.

KnownTagCodec gains the DIRECTION_* constants and
openTelemetryTagOf(id, direction). Direction-scoped OpenTelemetry names
are still not resolved when written by name.

This is the direction-neutral-id part of #12731's redesign; its
direction-scoped aliases and read-time resolution are left to that PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Name peer.port by the span's kind in OTLP export

With peer.port now one direction-neutral tag, its OpenTelemetry name
depends on the span: server.port on a client span, client.port on a
server span. The OTLP JSON and protobuf writers compute the span's
direction once from its span.kind and name each tag for it, so
direction-scoped renames apply too: peer.hostname exports as
server.address on a client span, and network.client.ip as
network.peer.address on a server span.

KnownTagCodec gains directionOf(TagMap) / directionOfKind(String), and
TagMap.EntryReader gains openTelemetryTag(direction), standing in for
the SpanTags helpers #12731 introduces.

Ported from #12731's redesign.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Let C2 inline the constructors of TagMap's nested classes

Entry's full constructor and the Internals and Ledger constructors were
private, so javac (targeting Java 8, without nestmates) gave each one a
synthetic access constructor taking the never-loaded class TagMap$1. C2
refuses to inline a call whose signature names an unloaded class
("unloaded signature classes"). So the Internals view the core creates
for each id write could not be scalar-replaced, and every Entry built
through that constructor went through a real call. Making the
constructors package-private removes the accessors and TagMap$1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Remove TagMap's remaining synthetic accessors and guard against them

Several private members were used across TagMap's nested classes, so
javac (targeting Java 8) generated access$ methods for them: Entry's
*2Prim helpers and requireKnownName, TagMap.parentEntryVisible,
TagMap.putEntry and the TagMap.buckets field. Each accessor adds a level
of inlining depth. putEntry's accessor sits on Internals.setKnown, the
core's trusted id-write path. All of these are now package-private.

TagMapSyntheticAccessorTest fails if TagMap or any of its nested classes
gains a synthetic accessor again. It is the prototype for the
@NoSyntheticAccess contract (APMLP-1976).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge master into id-tag-interceptor

Master now has #12715 (set-by-id, squashed) and #12037 (decorator
SpanPrototype). Conflicts resolve to this branch's newer id-keyed code,
plus:
- getAndSet finds its inherited prior by the entry's id (#12715's last fix);
  the shared-name test for it is dropped, since peer.port now has one id.
- #12037's resolveSplitByTagsPrecedence checks component by id and language
  by name against the serial-indexed split-by-tags table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Cover TagMap.Internals, KnownTagCodec and EntryReader paths

Brings the three classes back over jacoco's thresholds: the trusted
setters' Object/float/double overloads, isIntercepted, isKnown's range
edges, canonicalTagName, span-kind direction, and an entry's directional
OpenTelemetry name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Format the tag-registry generator to its spotless rules

build-logic is a separate Gradle build, so the root spotlessApply never
formats it. Three generator lines were wrapped where its Kotlin formatter
keeps them on one line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Order the tag-id carve bits by position

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge master into id-tag-interceptor

Brings #12814 (split-by-tags by OpenTelemetry name). Id matching
already covers OpenTelemetry names, so this PR's TagInterceptor wins and
#12814's withCanonicalNames goes, as planned. #12814's tests stay; one
expectation changes: with db.type configured, setTag("db.system", ...)
now splits too, since a known tag matches under any of its names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Re-apply split-by-tags language by its id

resolveSplitByTagsPrecedence checked language through the custom-tag
overload, which only matches names that resolve to no known tag.
language is known, so the check was always false and a prototype's
language was never re-applied over another split-by-tags candidate the
prototype set. Check and read it by LANGUAGE_ID, as for component.

Found by Codex, via a review comment from AlexeyKuznetsov-DD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Say what a tag id's intercepted bit does and doesn't decide

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Ratchet the tag constants toward full registry coverage

Every tag-name constant in Tags, InstrumentationTags and DDTags must be
in the tag registry, so instrumentation can set it by id, or be listed:

- NOT_TAG_NAMES: 19 constants that are tag values (span kinds, LLM
  Observability span kinds, the language value) or a name prefix.
- NOT_YET_REGISTERED: the 270 tag names not registered yet. Registering
  one fails the test until its entry is removed, so the list only
  shrinks; a new constant must be registered or listed.

62 of the 352 constants are registered today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Let the tracer overlay declare mixins on shared span types

The Java overlay could only declare routing-only keys, which belong to no
span type. It can now declare `mixins`, parsed like the shared file's,
for tags only this tracer sets (servlet's, Spring's, JMS's, ...). Since a
shared span type can't `include` a tracer's mixin by name, an overlay
mixin must push itself onto shared types with `applies`, and may not
reuse a shared mixin's name. Its tags resolve onto those types like any
other, so they get slots, and `intercepted` may list them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Register HTTP, DB, websocket and core internal tags

Each tag goes on the span types the code sets it on (production set
sites checked for every tag), so it gets a slot:

- http: http.endpoint, http.fragment.string (server and client).
- http.server: the http.forwarded.* headers and the security-testing
  request headers.
- db.client: db.schema, db.warehouse, the Cassandra/Couchbase/Redis
  specifics, db.command.*, and the DBM propagation tags.
- base: _dd.span_links and the debugger's _dd.code_origin.* (frame 0
  only, the only frame produced).
- trace_level: _dd.apm.enabled (process-static) and the local root
  span's _dd.trace_span_attribute_schema and _dd.profiling.ctx. Its
  comment now says it holds both kinds.
- New span types: web.handler (framework controller spans), and
  websocket receive/send/close with their message and close tags; the
  _dd.dm.* inherited-sampling tags are a mixin on receive and close.
- Java overlay mixins: servlet (servlet.context and servlet.path, moved
  out of the shared file), spring_webmvc (view.type on view.render),
  spring_webflux (handler.type, request.predicate on web.handler).

The coverage test's never-registered list becomes a map with a reason
per constant, gaining servlet.dispatch (an operation name), sql.query
(unused public API) and the two names only the serializers write.
Tags.DB_HOST was unreferenced, as constant and as literal, and goes.

43 more constants are registered (105 of 352); 221 remain. With
#12882's coloring the 109 tags use 41 slots, the size of http.server's
clique.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Register messaging, RPC and Data Streams tags

New shared span types, one per span kind, each messaging system adding
its tags with a mixin rather than a type of its own:

- messaging: producer, consumer (record.queue_time_ms,
  record.e2e_duration_ms), broker (time in queue; no direction, which
  none of these tags needs), client (AMQP commands that neither publish
  nor consume), and poll (kafka.records_count).
- Mixins: kafka (destination, bootstrap servers, cluster id, group,
  partition, offset, tombstone, Kafka Streams' processor.name) and amqp
  (command, exchange, routing key, queue, delivery mode, message.size).
- rpc: client and server, with the HTTP types' peer mixins; rpc.service,
  rpc.grpc.status_code, and message.type on gRPC message spans.
- Data Streams as mixins, not on base: pathway.hash on http, messaging
  and rpc spans; dsm.transaction.* on http and messaging producer and
  consumer spans.
- internal: an internal span no other type models. The Java overlay's
  tibco and mule mixins apply to it.
- bucket goes on db.client (Couchbase); the Java overlay's
  spring_webflux_client mixin puts message (why a request was
  cancelled) on http.client.

jms.produce was never set as a tag (JMSDecorator's operation name has
the same string), so it goes. message_bus.destination is unused in
production but TagInterceptorTest splits by it as a custom tag, so it is
listed as never registered.

29 more constants are registered; 190 remain, mostly CI Visibility and
AWS. With #12882's coloring the 138 tags use 44 slots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Register AWS SDK tags, with a span type per AWS service

AWS SDK calls are HTTP client spans, so aws.client extends http.client
and each modeled service extends it: aws.s3, aws.sqs, aws.sns,
aws.kinesis, aws.dynamodb, aws.sfn and aws.lambda, each with its own
tags (bucket and object, topic, stream, table, state machine and
execution, function). aws.client itself covers the other services.

A type per service, not a mixin per service: one aws.client holding
every service's tags would be 67 tags, over the 63-slot cap, though a
span never carries two services' tags. Mixins stay for what cuts across
span families: aws_request (agent, service, operation, endpoint, request
id) and aws_sqs_queue are also on the SQS consumer and broker spans,
which are messaging spans.

Also registered: the S3 Data Streams payload sizes
(http.request/response_content_length), and the span pointer inputs
(s3.eTag, dynamodb.primary_key_*), which SpanPointersProcessor removes
before serialization.

32 more constants are registered; 158 remain, mostly CI Visibility.
With #12882's coloring the 170 tags use 48 slots (aws.s3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Let the tracer overlay declare span types

Tags that extend one span concept belong on a span type; mixins are for
concerns that cut across span types or kinds (Data Streams, a messaging
system). The Java overlay could only declare mixins, so a Java-only
extension of a shared type (servlet's request, say) had to be one. It can
now declare `span_types`, parsed like the shared file's; each must
extend a span type and may not reuse a shared type's name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Model Java-only extensions as span types, not mixins

Mixins are for concerns that cut across span types or kinds. Each Java
overlay mixin extended exactly one shared type, so each becomes a Java
span type extending it: servlet.request (http.server), spring.view.render
(view.render), spring.webflux.handler (web.handler),
spring.webflux.client (http.client), tibco.process and mule.action
(internal). The registered tags are unchanged.

With #12882's coloring this saves a slot (47): servlet's and WebFlux's
tags are now in their subtypes' cliques only, not in every server and
client span's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Register code origin as a product mixin, without its frame names

Code origin is the debugger's add-on, set on entry spans (and their
local root) and @trace spans, not on every span. It moves from base to a
code_origin mixin on http.server, rpc.server, messaging.consumer and
internal, which also takes it out of every client, DB and AWS clique.

Only _dd.code_origin.type is registered. The frame tags are
_dd.code_origin.frames.<N>.<field>, a name pattern: an unbounded family
can't have ids ahead of time, so it can't use the dense store, and the
registry doesn't model patterns yet. Frame 0's six constants are listed
as never registered, with that reason. (Only frame 0 is produced today.)

With #12882's coloring the 164 tags use 40 slots, down from 47.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Register Elasticsearch, Twilio, JSP, Quartz and Hystrix tags; match patterns in the coverage test

These integrations' tags were thought unused, but only their constants
were: Elasticsearch, Twilio and JSP set them as string literals, and
Quartz and Hystrix use the constants. Each becomes a Java span type
(set sites checked per tag):

- elasticsearch.rest and elasticsearch.transport (db.client): the
  transport client's request, response and shard tags; the REST
  client's body and params, and its http.method and http.url.
- twilio.sdk (http.client), jsp.compile (internal), jsp.render
  (view.render), quartz.job and hystrix.command (internal).

Six names have no constant and are registered anyway:
elasticsearch.action, .request, .body, .params,
.shard.replication.successful, and jsp.requestURL. Tags set only by
literal are outside the coverage test's reach; APMLP-2046 proposes
auditing them in the instrumentation tests.

The coverage test now matches entries as patterns (`*`):
- NEVER_REGISTERED: a pattern excludes all it matches
  (Tags.SPAN_KIND_*, Tags.LLMOBS_*_SPAN_KIND, the code-origin frames).
- NOT_YET_REGISTERED_FAMILIES: a family, with a reason, excusing only
  its unregistered constants (Tags.TEST_*, Tags.GIT_*, Tags.CI_*,
  DDTags.CI_*, DDTags.TEST_*: CI Visibility, in a follow-up PR); stale
  once none of its constants is unregistered.
- NOT_YET_REGISTERED: the remaining single constants, still a strict
  ratchet.

26 single constants remain, plus the CI Visibility families. With
#12882's coloring the 203 tags use 55 slots (elasticsearch.transport).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Register the remaining APM tag constants

Set sites checked per tag:

- trace_level: process_id (a local root tag) and ai_guard.event (set on
  the local root of a trace holding an AI Guard evaluation span).
- base: events, the span events as JSON when the writer can't encode
  them natively, alongside _dd.span_links.
- exception_replay, a product mixin on every span:
  error.debug_info_captured.
- rpc: status.code and status.description (gRPC, set by literal).
- db.client: couchbase.operation_id (set by literal).
- Java servlet.request: user.principal (servlet containers) and timeout
  (Servlet 3 async timeouts); spring.webflux.client: event.

Never registered, each with its reason: thread.id and thread.name are
span fields the serializers write, never stored as tags;
runtime_version is a profiling and telemetry tag; t0 is a baggage key;
dd.env, dd.service and dd.version are log correlation (MDC) keys.

The CI Visibility constants among them (os.*, runtime.*,
_dd.host.vcpu_count, itr_correlation_id, pr.number, and library_version,
a span tag only on CI test traces) join the CI Visibility families.
That leaves no single unregistered constant: what remains is CI
Visibility, for a follow-up PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Split db.client into relational and per-system NoSQL types

db.client carried system-specific tags in every DB span's clique.
System-specific tags extend one concept, so they become types:

- db.client keeps what any database client sets: type, instance,
  operation, user, statement, and a command's outcome
  (db.command.cancelled and .results.count, set by Redis and Memcached).
- db.sql (JDBC, R2DBC): db.pool.name, db.schema, db.warehouse.
- db.cassandra, db.couchbase (seed nodes, bucket, operation id),
  db.redis (db.redis.dbIndex), and db.mongo.
- Database Monitoring is a product add-on, so its propagation tags
  become a dbm mixin on db.sql and db.mongo, where it injects comments.

With #12882's coloring the 213 tags use 47 slots, down from 57
(elasticsearch.transport).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge master into tag-registry-coverage

Brings #12817 (now merged, squashed as 62e9fe1). Its three
conflicts are squash artifacts: master's versions of those files equal
#12817's head, which this branch grew from, so this branch's versions
stand. The diff against master is exactly this branch's own changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Put span types first in the Java overlay

Span types are what instrumentation developers work with most, so they
lead the file, ahead of the routing-only keys and the intercepted list.
The header now describes both halves of the file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Add peer tags to messaging spans; apply code origin to handler spans

Two review findings, both confirmed in the code:

- RabbitChannelInstrumentation calls onPeerConnection on its client,
  producer and consumer decorators. messaging.producer and
  messaging.client now include peer, peer_address and outbound_peer
  (peer.service is computed for client and producer spans, as for
  http.client and rpc.client); messaging.consumer includes peer_address
  and inbound_peer. Consumers also set peer.hostname, which
  outbound_peer declares for client spans only: a TODO, as for
  websocket spans.
- Code origin's entry capture instruments annotated controller methods,
  whose active span is the framework's handler span, so the code_origin
  mixin also applies to web.handler.

Slots unchanged at 47 with #12882's coloring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge branch 'master' into dougqh/tag-registry-coverage

Merge master (#12882 slot coloring) into tag-registry-coverage

Clean merge. With the merged coloring, this branch's 212 registered tags
color into 47 slots (SLOT_COUNT 47, cap 63), and no tag shares a slot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
gh-worker-dd-mergequeue-cf854d Bot pushed a commit that referenced this pull request Oct 20, 2026
…ndary (otlp - tag registry - phase 3) (#12731)

Set known tags by id on TagMap and spans

Adds id-keyed setters, getters and removal to TagMap, TagMap.Entry and
spans, so a writer that knows the tag skips the name lookup. Squashed
from the review history of #12715.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Ignore unknown ids when clearing a tag by id; fix a comment wrap

span.setTag(0L, "") or a null value reached TagMap.getAndRemove(long),
which rejects unknown ids, so clearing threw IllegalArgumentException
while every id-keyed setter ignores an unknown id. removeTag(long) now
ignores it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Precheck and reuse the box in long-valued setters

The long-valued setters prechecked with needsIntercept, but when the
interceptor declined the tag they discarded the box it was given and stored
the primitive. Follow the same precheckIntercept -> setBox shape as the
other primitive setters, so TagMap keeps the box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Keep peer.port off the id path

peer.port is declared per direction in the registry (#12713), so it has no
direction-free id: BaseDecorator, shared by client and server decorators,
sets it by name, and the set-by-id tests use an unshared int tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge set-by-id (#12715) onto master

Mark intercepted tags with an id bit, via a tracer overlay

Set-path routing is a per-language concern, so it lives in a Java overlay
next to tag-conventions.yaml rather than in the language-agnostic
conventions. The overlay declares the keys that exist only to be routed
(resource.name, error, sampling directives, ...) and lists every tag
TagInterceptor may route. Each listed tag's id carries the INTERCEPTED bit
(bit 1), so a setter called with a constant id can fold the interception
test away. Serial numbers become public so TagInterceptor can switch on
them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Dispatch TagInterceptor on tag ids

TagInterceptor switches on the tag's serial rather than its name, so a
known tag is matched under any of its names and the *_OTEL_NAME cases go
away. needsIntercept(long) tests the INTERCEPTED bit first, which folds
away for a constant id. split-by-tags entries resolve to serials at
construction (each direction for a name declared per direction); only
custom tags are still matched by name. A test checks that exactly the
tags carrying the bit have a case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Set tags on spans by id first

A name setter resolves a known tag's name to its id once and sets it as the
id-keyed setters do; only a custom tag is still handled by name. The
id-keyed setters no longer look up the name to feed the interceptor, so with
a constant id the interception test folds to the INTERCEPTED bit. Entry
paths (builder ledger, prototypes, default tags) route by the entry's id
and precheck before boxing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Look up tag names by serial in an array, not a switch

The generated nameOf switched over every tag's serial: 491 bytes of
bytecode, too big for C2 to inline, so each id-keyed set paid two
out-of-line calls into it (the span's unknown-id guard and the entry's
name). Read a NAMES_BY_SERIAL array instead; nameOf is now 23 bytes and
inlines at every caller.

Also adds SetTagBenchmark (span setTag by constant id, non-constant id,
name, and custom name, against a bare and a synchronized TagMap store).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Point stale comments at the tracer overlay and the intercepted bit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Index the split-by-tags table by serial, sized to every known tag

The table is always allocated and never resized, so the split check -- the
only run-time check left on a non-intercepted constant-id set -- is a single
load, with no null or length test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Check for a known tag id with a range check, not a name lookup

The span's unknown-id guard called nameOf; KnownTagCodec.isKnown answers the
same question from the serial and a generated SERIAL_LIMIT, so it folds
away for a constant id. Also note that serials are not stable across
releases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Pin interceptTag(long) above C2's inlining size

DDSpanContext.setTag calls interceptTag only for intercepted tags. Kept out
of line (over FreqInlineSize, 325 bytes of bytecode), it never brings the
profiled handler bodies into setTag's compiled code, which would push setTag
past InlineSmallCode and stop callers inlining it -- the inlining a constant
id needs to fold its interception test. A test now fails if the switch
shrinks below the limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Tighten comments on the id-keyed interceptor

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Treat overlay tags as intercepted; tidy from review

An overlay tag exists only to be routed, so the generator now marks it
intercepted without a second listing; intercepted: lists only the tags
declared in tag-conventions.yaml that the tracer also routes. Also restore
the doc comment and @Suppress that the overlay helper displaced, read the
serial limit one way, inline isUnknownTag, and drop an unused default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Reject shared-name tag ids on the id-keyed TagMap and span paths

A tag declared once per direction (peer.port) has two ids but one
shared Datadog name, and keyOf resolves that name to neither. An entry
keyed by one of those ids hashed by the id while name-based access
hashed the name as a custom tag, so get/remove/containsKey by name
missed it and a later set by name produced a duplicate key.

Encode a SHARED_NAME flag in the reserved id bit 0 so the check folds
for a constant id. TagMap's id-keyed create/get/remove reject such ids,
and DDSpanContext's id-keyed setters ignore them, as they do unknown
ids, until name resolution knows the span's direction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Test rejection of shared-name tag ids

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Build custom tags by name and known tags by id

Entry's package-private name factories are now custom-only: they skip the
registry and assert the name is not a known tag. Callers that start from a
name resolve it once -- TagMap.set(String, ...) forwards a known tag to
set(long, ...), while getAndSet, put, Ledger.set, putAll and the public
Entry.create(String, ...) API go through anyEntryFor and friends -- so a
custom tag pays one registry lookup instead of two.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Resolve direction-dependent tag names by the span's kind

Builds on the registry directions (#12713) and id-keyed setters
(#12715, included here): KnownTagCodec resolves names by direction,
TagMap finds a shared name's tag whichever direction it holds, and
DDSpanContext resolves direction-dependent names by the span's kind.
Squashed from the review history of #12731.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Key shared-name tags by direction now that resolution knows it

#12817 rejected shared-name ids (peer.port inbound/outbound) on the id
paths until name resolution could see a span's direction. With direction
resolution in place, accept them again: a read or removal by the shared
name already checks each direction's id. To keep one entry per tag, a write
by the shared name stores under whichever direction's tag the map holds
(otherwise under the name, which the span re-keys once it has a
direction), and a write by id drops a value held under the name alone.
isKeyableById goes; the SHARED_NAME bit stays and keeps the new check
foldable for a constant id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge master into id-tag-interceptor (#12817)

# Conflicts:
#	.github/CODEOWNERS

Merge id-tag-interceptor (#12817) into tag-registry-direction-resolution

Give the tracer core a restricted, trusted path into TagMap

TagMap.internals() returns a small Internals view whose setKnown(...) skips
the id validation the checked set(long, ...) repeats; DDSpanContext's
id-keyed setters, which have already checked isKeyableById, now use it.
Internals is created per call, so escape analysis removes it.

internals() is marked with a new @restricted(allowedIn = ...) annotation,
and gradle/forbiddenApiFilters/instrumentation.txt bans it in
instrumentation modules, so calling it there fails forbiddenApisMain.
Internals is also where the batched setTagsFrom/TagSink writes will go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Mark TagMap.Internals @NoEscape

It exists to be created, used within the expression, and scalar-replaced;
storing one would force a real allocation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge id-tag-interceptor (#12817) into tag-registry-direction-resolution

Point Internals' trust note at isKnown, the span's check here

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Store each direction-dependent tag once and apply direction on read

peer.port gets one direction-neutral id (PEER_PORT_ID); only its
OpenTelemetry name (client.port inbound, server.port outbound) depends on
the span's direction. The generator merges per-direction declarations
into one tag, and the SHARED_NAME bit and shared-name sentinel go away.

Direction-scoped OpenTelemetry names (server.address, client.port, ...)
are stored as written. SpanTags resolves them by the rule
(name, direction) -> tag when a span reads or removes a tag, with the
tag winning over its scoped name. processTagsAndBaggage then folds them
into their tags once the kind is final. This removes the
unresolvedDirectionalTags deferral and re-keying from DDSpanContext, and
with them:
- bulk/builder writes bypassing resolution
- name-vs-id hash mismatch and parent shadowing in TagMap
- Entry equals/hashCode disagreement (equals now compares tagHash)
- the race between a deferred write and the first kind
- alias write order and kind changes not re-keying

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Name direction-dependent tags by span kind in OTLP export

The OTLP JSON and protobuf writers compute the span's direction once from
its span.kind (SpanTags.directionOf) and name each tag for it, so
peer.hostname exports as server.address on a client span, and
network.client.ip as network.peer.address on a server span.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Apply last write wins between a tag and its direction-scoped alias

With no ordering state (a TagMap has no insertion order):
- Once the span has a kind, an alias write (server.address on a client
  span) is stored as its tag, overwriting it.
- Before then, the alias is stored as written. A write of a tag that has
  an alias removes every pending alias that could denote it, so a
  pending alias still present is the newer write, and it wins on read
  and when processTagsAndBaggage folds it into the tag.

Every DDSpanContext write path (by name, by id, by entry, and in bulk)
goes through SpanTags.prepareWrite. Names that don't depend on direction
cost one small map probe; ids that don't, one array load. The accepted
corner (a pre-kind write of either possible tag drops a pending alias) is
documented in SpanTags and DDSpanContext.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Carry the direction role in tag ids so plain setTag pays nothing extra

748a8af added a map probe to every write by name and routed bulk
copies entry by entry. Now:
- The generator sets a HAS_SCOPED_ALIAS bit (bit 0) on each tag with a
  direction-scoped OpenTelemetry name (peer.hostname, http.hostname,
  network.client.ip, peer.port). A mask tests it, and that folds away for
  a constant id.
- A write by name does one lookup, KnownTagCodec.keyOrAliasOf, which
  returns the id (bit included) or the existing alias sentinel. It then
  stores through the new TagMap.setResolved without a second lookup. Only
  ids with the bit, or the alias sentinel, go through SpanTags.
- Bulk copies use putAll again. needsIntercept(TagMap), computed once per
  tracer-level map, is also true for a map with a direction-dependent
  name. Per-entry paths test the id bit, or a 64-bit hash filter of the
  aliases for a custom entry.

The last-write-wins semantics and their tests are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Move alias superseding off the setTag store fast path

The HAS_SCOPED_ALIAS test in the id setters cost about 5 ns on setTag by
name. That chain sits at C2's InlineSmallCode limit; with the extra code,
DDSpan.setTag "failed to inline: already compiled into a big method".

TagInterceptor.needsIntercept(long) now tests INTERCEPTED and
HAS_SCOPED_ALIAS with one mask, the same instruction count as before.
interceptTag, which stays out of line, calls
DDSpanContext.supersedeAliases for a tag with the bit and still lets
the tag be stored. The store fast path is back to exactly its pre-alias
shape. Tags with an alias pay a second lock acquisition, and primitive
writes of them (peer.port) box, as an intercepted tag's do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Cover SpanTags and the KnownTagCodec direction helpers in internal-api

internal-api's jacocoTestCoverageVerification failed. SpanTags had no
internal-api tests (0% instructions and branches), and KnownTagCodec
fell to 70% instructions and 50% branches against its 80%/70%
thresholds. SpanTagsTest exercises get, remove, prepareWrite,
prepareAliasWrite, isDirectionDependent, resolveScopedNames and
openTelemetryTagOf across every direction, with the alias present and
absent and the last-write-wins cases. KnownTagsTest covers the
direction-scoped-name, flag and range helpers. SpanTags is now at
99%/96% and KnownTagCodec at 100%/94%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Route alias-target writes only on spans holding a pending alias

dc06725 routed every write of a tag with a direction-scoped alias
(peer.hostname, peer.port, ...) through interception: a second lock, and
boxing for primitive writes. That's about 31 ns for setTag(peer.hostname).

Each span now keeps a routing mask, routedTagFlags, which holds
INTERCEPTED until an alias is stored as written; only then does it gain
HAS_SCOPED_ALIAS. The id setters test it with the same single mask test
as before (TagInterceptor.needsIntercept(tagId, flags)). So on a span
that never held a pending alias, the common case, these writes take the
plain store path, unboxed. The flag is sticky, set for any alias stored
as written, so whenever a pending alias could exist the write is routed
exactly as before. It's written under the tag lock and read without it:
a setter that misses it is racing an unordered alias write.

Adds SetTagBenchmark arms for the routed path and for a primitive
peer.port write by id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge tag-registry-direction-resolution into the single-representation rework

# Conflicts:
#	internal-api/src/test/java/datadog/trace/api/TagMapSetByIdTest.java

Cover the remaining TagMap.Internals setKnown overloads

jacocoTestCoverageVerification flagged TagMap.Internals at 0.6: the
existing test exercised the CharSequence, int, long and boolean setters;
this adds the Object, float and double ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Add datadog.apicontract to CODEOWNERS

Restricted lives in the new datadog.apicontract package, which had no owner.
Give it the same owner as its sibling datadog.perfcontract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge branch 'master' into dougqh/id-tag-interceptor

Give peer.port one direction-neutral id

peer.port was modeled as one tag per direction: PEER_PORT_INBOUND_ID and
PEER_PORT_OUTBOUND_ID, sharing the Datadog name and marked with a
SHARED_NAME id bit. Two ids under one name kept producing lookup and
supersede bugs: keyOf("peer.port") resolved to neither id, so a value set
by name and one set by id landed in different entries, and tombstones and
getAndSet by name could not find what an id had stored.

The generator now merges a name's per-direction declarations into one tag
with one id (PEER_PORT_ID), and only its OpenTelemetry name depends on the
span's direction: openTelemetryNameOf(id, direction) returns client.port
inbound and server.port outbound. The SHARED_NAME bit, hasSharedName and
isKeyableById go away (bit 0 is left unused), and keyOf("peer.port")
returns PEER_PORT_ID.

KnownTagCodec gains the DIRECTION_* constants and
openTelemetryTagOf(id, direction). Direction-scoped OpenTelemetry names
are still not resolved when written by name.

This is the direction-neutral-id part of #12731's redesign; its
direction-scoped aliases and read-time resolution are left to that PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Name peer.port by the span's kind in OTLP export

With peer.port now one direction-neutral tag, its OpenTelemetry name
depends on the span: server.port on a client span, client.port on a
server span. The OTLP JSON and protobuf writers compute the span's
direction once from its span.kind and name each tag for it, so
direction-scoped renames apply too: peer.hostname exports as
server.address on a client span, and network.client.ip as
network.peer.address on a server span.

KnownTagCodec gains directionOf(TagMap) / directionOfKind(String), and
TagMap.EntryReader gains openTelemetryTag(direction), standing in for
the SpanTags helpers #12731 introduces.

Ported from #12731's redesign.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge id-tag-interceptor (#12817, before the peer.port move) into tag-registry-direction-resolution

Brings #12817's master merge and its CODEOWNERS entry for
datadog.apicontract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge id-tag-interceptor (#12817) into tag-registry-direction-resolution

#12817 now carries the direction-neutral peer.port id and naming peer.port
by span kind in OTLP export (22d0fd1, ba715de), moved down from this
branch's redesign. This branch already has the final versions of those
changes (06dec0f, d469c66, with SpanTags in place of #12817's
stand-ins), so the merge keeps this branch's tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Let C2 inline the constructors of TagMap's nested classes

Entry's full constructor and the Internals and Ledger constructors were
private, so javac (targeting Java 8, without nestmates) gave each one a
synthetic access constructor taking the never-loaded class TagMap$1. C2
refuses to inline a call whose signature names an unloaded class
("unloaded signature classes"). So the Internals view the core creates
for each id write could not be scalar-replaced, and every Entry built
through that constructor went through a real call. Making the
constructors package-private removes the accessors and TagMap$1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Remove TagMap's remaining synthetic accessors and guard against them

Several private members were used across TagMap's nested classes, so
javac (targeting Java 8) generated access$ methods for them: Entry's
*2Prim helpers and requireKnownName, TagMap.parentEntryVisible,
TagMap.putEntry and the TagMap.buckets field. Each accessor adds a level
of inlining depth. putEntry's accessor sits on Internals.setKnown, the
core's trusted id-write path. All of these are now package-private.

TagMapSyntheticAccessorTest fails if TagMap or any of its nested classes
gains a synthetic accessor again. It is the prototype for the
@NoSyntheticAccess contract (APMLP-1976).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge id-tag-interceptor (#12817) into tag-registry-direction-resolution

Brings #12817's removal of TagMap's synthetic accessors: package-private
constructors for its nested classes, plus putEntry, buckets and the Entry
helpers, guarded by TagMapSyntheticAccessorTest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge master into id-tag-interceptor

Master now has #12715 (set-by-id, squashed) and #12037 (decorator
SpanPrototype). Conflicts resolve to this branch's newer id-keyed code,
plus:
- getAndSet finds its inherited prior by the entry's id (#12715's last fix);
  the shared-name test for it is dropped, since peer.port now has one id.
- #12037's resolveSplitByTagsPrecedence checks component by id and language
  by name against the serial-indexed split-by-tags table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Cover TagMap.Internals, KnownTagCodec and EntryReader paths

Brings the three classes back over jacoco's thresholds: the trusted
setters' Object/float/double overloads, isIntercepted, isKnown's range
edges, canonicalTagName, span-kind direction, and an entry's directional
OpenTelemetry name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Format the tag-registry generator to its spotless rules

build-logic is a separate Gradle build, so the root spotlessApply never
formats it. Three generator lines were wrapped where its Kotlin formatter
keeps them on one line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge id-tag-interceptor (#12817) into tag-registry-direction-resolution

Brings master, including #12715 (now merged) and Spotless 8.10.4, plus
#12817's new tests and its tag-registry formatting fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Drop #12817's direction stand-in tests, covered by SpanTagsTest

#12817 tests KnownTagCodec.directionOf/directionOfKind and
EntryReader.openTelemetryTag(int), stand-ins that this branch replaces
with SpanTags. SpanTagsTest already covers the same cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Order the tag-id carve bits by position

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge master into id-tag-interceptor

Brings #12814 (split-by-tags by OpenTelemetry name). Id matching
already covers OpenTelemetry names, so this PR's TagInterceptor wins and
#12814's withCanonicalNames goes, as planned. #12814's tests stay; one
expectation changes: with db.type configured, setTag("db.system", ...)
now splits too, since a known tag matches under any of its names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge id-tag-interceptor (#12817) into tag-registry-direction-resolution

Brings master, including #12814, and #12817's carve-bit reordering,
applied here with HAS_SCOPED_ALIAS: bits 0, 1, 2 in order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Re-apply split-by-tags language by its id

resolveSplitByTagsPrecedence checked language through the custom-tag
overload, which only matches names that resolve to no known tag.
language is known, so the check was always false and a prototype's
language was never re-applied over another split-by-tags candidate the
prototype set. Check and read it by LANGUAGE_ID, as for component.

Found by Codex, via a review comment from AlexeyKuznetsov-DD.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Say what a tag id's intercepted bit does and doesn't decide

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge #12817's final commits into tag-registry-direction-resolution

Merge remote-tracking branch 'origin/master' into dougqh/tag-registry-direction-resolution

Restore the conventions' one-tag wording for peer.port

An earlier merge kept the comments from before peer.port got a single,
direction-neutral id. The generated registry already has one
PEER_PORT_ID; the comments now say so again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Tidy the direction-resolution code from review

- TagMap: getAndSet(String, ...) and put use the existing entry
  helpers again instead of inlining them; writeKeyOf is gone.
- DDSpanContext: build the span-kind direction table from
  SpanTags.directionOfKind so the two can't drift, and keep the
  supersede-pending-aliases guard in one helper.
- KnownTagCodec: drop keyOf(String, int), used only by tests, and the
  package-private lookup that duplicated keyOrAliasOf. The tests check
  the same behaviour through directionalKeyOf and keyOrAliasOf.
- TagConventions: drop the unused sharedNameDirection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Keep the alias slow paths to spans that really hold a pending alias

- Serialization resolves pending aliases only on a span that stored
  one, instead of walking every tag of every span, and skips internal
  spans, where no alias names a tag.
- A custom tag that merely passes the alias filter no longer marks the
  span as holding a pending alias. prepareAliasWrite now tells a real
  alias left pending (the registry's sentinel) from a name that is no
  alias at all, and only the former sets the flag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Make a removal before the span's kind supersede every meaning

Removing a direction-dependent name before the span had a kind removed
only its literal spelling, so an older value could resurface once the
kind arrived: a removed server.address left peer.hostname visible on what
became a client span, and removing peer.hostname left a pending
server.address that then resolved to it. A removal now counts as a
write: removing a tag also removes the pending aliases that could denote
it, and removing an alias also removes every tag it could denote.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Extend the pending-alias gate to reads, removals and entry routing

- getTag of a tag with an alias probes for a pending alias only when
  the span holds one; SpanTags.get takes the span's flag.
- Removal by name does one keyOrAliasOf and dispatches on it, with a
  new TagMap.removeResolved, instead of two registry lookups.
- Entries are routed through interceptTag only for real routes;
  storeEntry already supersedes pending aliases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Encapsulate direction handling in a package-private DirectionalTags

SpanTags was a public internal-api class, though only DDSpanContext used
its span behavior. It is now DirectionalTags, package-private beside
DDSpanContext in dd-trace-core, and reads everything through
KnownTagCodec and TagMap.

The naming facts other code needs go back to KnownTagCodec, where master
had them: directionOf, directionOfKind, and a new isDirectionDependent
for TagInterceptor. TagMap.EntryReader.openTelemetryTag(int) is restored,
so the OTLP writers are unchanged from master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Write and read known tags by id without building an Entry per call

- TagMap.setResolved now calls set(tagId, ...) for a known id, as
  set(String, ...) already does, so every name-keyed span write takes the
  id path. Same result on today's store; a store that keeps known tags
  without an Entry per tag no longer allocates one here.
- TagMap gains a package-private getKnownValue(long), exposed to the core
  as Internals.getKnownValue. DirectionalTags' id reads and
  KnownTagCodec.directionOf use it instead of getEntry(id).objectValue().

Both pulled forward from the dense store work (#12856) so it overrides
them rather than reworking these call sites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Resolve aliases when the kind is set, and route them as their tag

- Pending aliases fold into their tags as soon as the span learns its
  kind, in setSpanKindOrdinal, instead of at serialization. Readers that
  run first, such as peer service naming in processServiceTags, now see
  the tag. The serialization-time pass is gone.
- Applying a prototype checks presence by the tag an entry resolves to,
  so an alias default can't overwrite an explicit value of its tag.
- A name-keyed or bulk write of an alias, once the span has a
  direction, is intercepted as the tag it denotes, so split-by-tags on
  peer.hostname sees server.address.

Accepted corner, documented on DirectionalTags: an alias written before
the kind is resolved without passing through the interceptor again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Close three more gaps between the write paths and direction handling

- Builder ledger removals go through DirectionalTags.remove, as
  removeTag does, so removing an alias also removes the tag a
  direction-aware write stored it as.
- Split-by-tags that names an alias itself keeps that alias on the
  custom path, so the configured name still matches after aliases
  started routing as the tag they denote.
- setSpanKindOrdinal checks for pending aliases under the lock alias
  registration takes, so a concurrent alias write can't be left
  unresolved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Let a Datadog tag name double as another tag's direction-scoped alias

The registry rejected any OpenTelemetry name that matched a Datadog name,
even for direction-scoped renames. That guarded only against misuse (a tag
landing on a span of the wrong direction and colliding at export), and it
forced invented names such as http.port for the inbound server port.

A scoped rename may now be another tag's Datadog name. The owning tag keeps
the name in every direction no alias claims: server.port is peer.port on
outbound spans and its own tag (the server's port) otherwise. The name goes
through the existing direction-scoped alias handling, so writes resolve by
the span's kind. The one remaining collision, the tag written by id on a
span where its name belongs to another tag, is handled at OTLP export: the
alias's tag wins, so each attribute name is emitted once.

Direction-free renames still may not match a Datadog name, since without a
direction such a name would resolve to two tags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Drop the OTLP export check for a tag written by id on the wrong side

Only a by-id write can put a shared-name tag on a span where its name
belongs to another tag (server.port's own tag beside peer.port on a client
span); name writes always resolve by the span's kind. Ids are trusted, so
that case is exported as written rather than reconciled on every span.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge remote-tracking branch 'origin/master' into dougqh/tag-registry-direction-resolution

Resolve direction-scoped names at the OpenTelemetry boundary, not in the core

Replaces the span-level direction handling (DirectionalTags, pending
aliases folded in when the kind arrives, last-write-wins between a tag and
its alias, the HAS_SCOPED_ALIAS id bit and the routing it drove) with a
smaller rule: in Datadog terms a tag's name never depends on the span's
direction, so the core resolves names without one, as on master. Only some
OpenTelemetry names depend on direction, and direction is applied where the
OpenTelemetry side knows it:

- Export already names each tag by the span's direction.
- The generator emits KnownTags.Inbound, Outbound and Internal: each
  OpenTelemetry name that names a tag in that direction, as the tag's id
  (Outbound.SERVER_ADDRESS_ID is PEER_HOSTNAME_ID). Instrumentation that
  knows its direction writes in OpenTelemetry terms by id.
- KnownTagCodec.directionalKeyOf resolves such a name for a direction.
- The OpenTelemetry shim stores a direction-scoped attribute as the tag it
  denotes for its span's kind; the builder holds one set before the kind
  until the span starts.

Nothing in the tracer writes these names by string, so the pending-alias
machinery only served user code writing an alias before setting the kind;
such a value now stays as written. server.port stays a Datadog name too:
the server's own port, and peer.port on outbound spans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge master (#12882 slot coloring) into tag-registry-direction-resolution

Clean textual merge. Also formats TagRegistryGeneratorTest's holder
helper, which build-logic's spotless flagged (the root spotlessApply
skips build-logic).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Resolve held OTel attributes as soon as the span kind is set

An attribute set before the kind was replayed at start, over any later
write to the same attribute. Resolving the held attributes when the kind
is set keeps them in write order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Ignore null OTel attribute keys again

A null key reached the generated directional switch and threw into
application code. Resolve it as no known tag, so the span builder drops
it as it did before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Merge branch 'master' into dougqh/tag-registry-direction-resolution

Merge branch 'master' into dougqh/tag-registry-direction-resolution

Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
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: feature Enhancements and improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants