Conversation
TagMap.set(long tagId, value) builds the entry under the known tag's canonical name from KnownTagCodec.nameOf, skipping the canonicalizing name lookup the String setters pay. An id that names no known tag is rejected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AgentSpan gains setTag(long tagId, ...) defaults that resolve the id to its name and delegate, so every implementation stays correct. DDSpan and DDSpanContext override them with the same behavior as the String family -- interception runs on the tag's name, and the http.status_code quirk is kept -- but store the tag by id. Follows the API shape of #11901, implemented on the current TagMap rather than the dense store, so the dense store can later swap the implementation behind the same signatures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
onPeerConnection and setPeerPort run on every client span; they now set peer.hostname, peer.ipv4/ipv6 and peer.port by KnownTags id. First caller of the id-keyed setTag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The decorator tests mock AgentSpan, so they observe the id-keyed call directly rather than the String call the default would delegate to. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TagMap.Entry now 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 is in bits 63-48, so the two can never collide. The canonicalizing keyOf the String constructor already pays yields the id, so tagId() becomes a field read instead of a lookup, and 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, and Entry.hashCode() keeps returning the bucket hash, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
The id constructor validated the id with one nameOf and named the entry with another, so every id-keyed insert paid two resolver switches. On TagMapInsertBenchmark that made knownById 17% slower than before the tag-hash change; resolving once makes it 6% faster. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Inserts a span's worth of tags by Datadog name, by id, and as custom tags, single-threaded with -prof gc. Records the before/after of hashing entries by tag id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| String ip = remoteAddress.getHostAddress(); | ||
| if (resolved && Config.get().isPeerHostNameEnabled()) { | ||
| span.setTag(Tags.PEER_HOSTNAME, hostName(remoteAddress, ip)); | ||
| span.setTag(KnownTags.PEER_HOSTNAME_ID, hostName(remoteAddress, ip)); |
There was a problem hiding this comment.
These are included because eventually we'll need to use ID to disambiguate some of these concepts for OTLP, and I wanted to illustrate usage.
That said, I'm happy to leave these to another PR if others prefer.
The before/after comparison lives in the PR description. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🟢 Java Benchmark SLOs — All performance SLOs passed
PR vs. master results
Commit: Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion. |
Entry.create is the public way to build an entry ahead of time, at the same layer as AgentSpan.setTag, so it gets the same id-keyed overloads: same contract as the name-keyed ones (a null or empty value yields no entry), without the name lookup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 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".
A Spock mock of AgentSpan intercepts setTag(long, ...) directly, so every call site moved to KnownTags ids broke its test's expectations even though the tag set was the same. NameKeyedAgentSpan makes the id-keyed overloads final and delegates them to the name-keyed ones; a mock cannot override a final method, so it only ever sees setTag(name, value). BaseDecoratorTest gains mockSpan(), and the decorator specs use it. This replaces the per-call-site expectation edits: the specs' expectations are back to tag names, and moving another call site to ids needs no test change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BucketGroup treats a zero hash as a vacant slot. Folding a tag id to its bucket hash yields zero when the serial equals the id's flag bits: _dd.djm.enabled has serial 4 and the trace-level bit, 4. Sharing a bucket group, it then looked empty -- a later insert could overwrite it, and size and copy skipped it. A folded zero now maps to the nonzero sentinel the name hash already uses. Found by Codex review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Checks every generated id, and inserts every known tag into one map and a copy. Both fail without the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ougqh/tag-id-setters


What Does This Do
Adds setting a known tag by its id to
TagMapand 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)forObject,CharSequence, and the primitives. The entry is built under the tag's canonical name fromKnownTagCodec.nameOf, skipping the canonicalizing name lookup theStringsetters 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 asAgentSpan.setTag. It has the same contract as the name-keyedcreate: 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 theStringsetter, so everyAgentSpanimplementation stays correct with no changes.DDSpan/DDSpanContextoverrides with the same behavior as theStringfamily:TagInterceptorstill runs on the tag's name (it is name-keyed), primitives still box only when the tag might be intercepted, and thehttp.status_codequirk is kept. The difference is that a stored tag is set by id.TagMaphashes entries by tag id.TagMap.Entryholds 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 akeyOflookup.getEntry,getAndRemove) take onekeyOf, instead of canonicalizing and then hashing the name separately.BucketGroupkeeps itsinthashes, so onlyEntrygrows:inttolongcosts 8 bytes per entry because of alignment. That is an accepted temporary cost until the dense store.BaseDecorator.onPeerConnection/setPeerPort, which run on every client span, now setpeer.hostname,peer.ipv4/ipv6andpeer.portbyKnownTagsid.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.portbecomes 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:It follows the API shape of #11901, but implemented on the current
TagMaprather than the dense store, which #11901 is stacked on. The dense store can later swap the implementation behind the same signatures.Additional Notes
Ledger/builder support. Each will arrive with a caller.Mapcontract unchanged, deliberately:Entry.hashCode()still returns the bucket hash rather thankey.hashCode() ^ value.hashCode(), andTagMapstill compares by identity. Both were already the case before this PR; changing them is a separate behavior change.PEER_PORT_OUTBOUND_ID) hashes by that id, so a lookup by the shared name"peer.port"will not find it. Phase 2 resolves names by direction before reachingTagMap, which keepsTagMapdirection-agnostic.peer.port,KnownTags.PEER_PORT_IDno longer exists, soBaseDecorator.setPeerPortstops compiling until it picks a direction. That is intended: it forces the direction to be chosen explicitly.PEER_PORT_ID.setPeerPort:peer.hostname/ipv4/ipv6keep the same ids in Scope OpenTelemetry tag names by span direction in the tag registry #12713, soonPeerConnectionstays a real caller with no coupling.peer.port, which server decorators also set for the client's port, moves to the direction-aware follow-up.peer.portinto a tag per direction, a tag stored under a per-direction id hashes by that id, but the shared name"peer.port"resolves to no id, so a lookup by name misses it. IfsetPeerPortswitched toPEER_PORT_OUTBOUND_ID, a latergetTag("peer.port")would come back empty. Until direction-aware reads land,setPeerPorttherefore has to keep settingpeer.portby name, which stores it as a custom tag that name lookups still find. So whichever of the two PRs merges second keepssetPeerPortname-keyed;onPeerConnection'speer.hostname/ipv4/ipv6keep the same ids in Scope OpenTelemetry tag names by span direction in the tag registry #12713 and are unaffected.AgentSpanwould see the id-keyed call directly, since a mock intercepts the default method, so every call site moved to ids would break its test's expectations. Instead, the decorator specs now mockNameKeyedAgentSpan, a test-onlyAgentSpanwhose id-keyed overloads arefinaland delegate to the name-keyed ones. A mock can't override a final method, so it only seessetTag(name, value).BaseDecoratorTest.mockSpan()creates one. The specs' expectations are unchanged from master, and moving further call sites to ids needs no test change. (Spock only allowsMock()inside a spec class, so the factory lives on the base spec rather than in a trait. Other modules' 50-oddMock(AgentSpan)sites can adopt it as they migrate.)Tags.PEER_PORTand friends stay as they are; a@Deprecatedpointing at the ids comes once callers have migrated.Contributor Checklist
:internal-api:test(TagMap*incl.TagMapFuzzTest, 384 tests;KnownTags*; newTagMapSetByIdTest,TagMapTagHashTest,TagMapEntryCreateByIdTest),:internal-api:spotlessCheck:dd-trace-core:test(DDSpan*incl. newDDSpanSetTagByIdTest,*taginterceptor*,*otlp*,common.writer.*,common.metrics.*; 1279 tests),:dd-trace-core:spotlessCheck.DDAgentWriterCombinedTest.unixSocketTimeoutKeepsWorkerAliveAndReconnectsfailed 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:knownByNameknownByIdcustomByNameThe first run caught a regression:
knownByIdwas +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:spotlessCheckJira ticket
N/A
🤖 Generated with Claude Code