Repository navigation
Conversation
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>
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>
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>
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>
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>
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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
# Conflicts: # .github/CODEOWNERS
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
CI Visibility Test Environment - nebula-release-pluginJob Status: 🟢 success
Baseline: median of Tests run to verify that CI Visibility behavior has not regressed in the current PR. |
CI Visibility Test Environment - reactive-streams-jvmJob Status: 🟢 success
Baseline: median of Tests run to verify that CI Visibility behavior has not regressed in the current PR. |
CI Visibility Test Environment - pass4sJob Status: 🟢 success
Baseline: median of Tests run to verify that CI Visibility behavior has not regressed in the current PR. |
CI Visibility Test Environment - netflix-zuulJob Status: 🟢 success
Baseline: median of Tests run to verify that CI Visibility behavior has not regressed in the current PR. |
CI Visibility Test Environment - heliboardJob Status: 🟢 success
Baseline: median of Tests run to verify that CI Visibility behavior has not regressed in the current PR. |
🟢 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. |
CI Visibility Test Environment - sonar-kotlinJob Status: 🟢 success
Baseline: median of Tests run to verify that CI Visibility behavior has not regressed in the current PR. |
CI Visibility Test Environment - jolokiaJob Status: 🟢 success
Baseline: median of Tests run to verify that CI Visibility behavior has not regressed in the current PR. |
CI Visibility Test Environment - okhttpJob Status: 🟢 success
Baseline: median of Tests run to verify that CI Visibility behavior has not regressed in the current PR. |
CI Visibility Test Environment - spring_bootJob Status: 🟢 success
Baseline: median of Tests run to verify that CI Visibility behavior has not regressed in the current PR. |
Replaces the `slot: shared` markers with requirement levels. The tags TestDecoratorImpl.afterStart puts on every CI span (the CI provider and commit) and the test source range are recommended rather than optional, so the coloring keeps them on exact slots; the tags set only some of the time stay optional and absorb the overflow on test spans. With #12882's coloring, the 306 tags fit in 63 slots; only the CI test span types, which carry 73 to 104 tags, have tags sharing a slot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI Visibility Test Environment - sonar-javaJob Status: 🟢 success
Baseline: median of Tests run to verify that CI Visibility behavior has not regressed in the current PR. |
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>
…registry-civis Clean merge; the generator now matches #12893's, with no trace of the reverted slot marker. With the merged coloring the 319 tags color into 63 slots: the CI test span types overflow, and 66 optional tags share slots (CI Visibility's occasional tags plus _dd.span_links, _dd.svc_src, error.debug_info_captured and events), on those span types only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ed3d91a464
ℹ️ 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".
| - { dd-name: test_session_id, type: long, required: required } # hoisted to a top-level field by the CI intake mapper | ||
| - { dd-name: _dd.host.vcpu_count, type: int, required: recommended } | ||
| - { dd-name: _dd.ci.env_vars, type: string, required: recommended } | ||
| - { dd-name: library_version, type: string, required: recommended } # every span of a CI trace |
There was a problem hiding this comment.
Model library_version on every span in CI traces
When a CI test produces ordinary child spans or JUnit callback spans, CiVisibilityTraceInterceptor.onTraceComplete adds library_version to every span in the trace, not only to session/module/suite/test events. Declaring it solely under ci.span therefore omits its co-occurrence with regular internal, HTTP, database, and other span tags from the coloring graph; the generated assignment can reuse its slot for a tag that is present on such a child span, defeating the exact presence bit and forcing dense-store lookups to scan. Model this as a CI-gated tag applying to all span types rather than as a ci.span-only tag.
Useful? React with 👍 / 👎.
| - { dd-name: test.test_management.is_attempt_to_fix, type: boolean, required: optional } | ||
| - { dd-name: test.test_management.attempt_to_fix_passed, type: boolean, required: optional } | ||
| - { dd-name: test.retry_reason, type: string, required: optional } | ||
| - { dd-name: itr_correlation_id, type: string, required: optional } |
There was a problem hiding this comment.
Register the library capability tags
Whenever CI Visibility advertises library capabilities, TestImpl sets the finite _dd.library_capabilities.<capability> tags produced by LibraryCapability.asTag(), but none of those ten names is declared in either conventions file. Because the coverage test only reflects constants from Tags, InstrumentationTags, and DDTags, it still passes while KnownTagCodec.keyOf returns zero for these live test tags, leaving them on the custom-tag path and making the stated ID/dense-layout migration incomplete. Add the enum's capability keys as optional string tags on test and include this generated family in registry coverage.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
More details
The registered CI IDs and APM-only OS/runtime tags match their setter usage. Registry generation, compilation, and direct tag-map checks supported compatibility.
🤖 Bits Code Review · Commit ed3d91a
What Does This Do
Registers the CI Visibility tags in the tag registry, on span types for test sessions, modules, suites and tests, with requirement levels that tell the slot coloring which tags are set only some of the time. With #12893, every tag constant is now registered or deliberately excluded.
span.kindvalues (test_session_end,test_module_end,test_suite_end,test) have no direction. Each tag's production set sites were checked for which of the four it lands on.ci.span(abstract): what standard enrichment (TestDecoratorImpl.afterStart) puts on every CI span: the session name, test type, status, framework, session id, and the CI provider and commit (ci.*,git.*).ci.aggregate(abstract, sessions and modules): command, coverage, ITR skipping counts, early flake detection, test management.test.session,test.module,test.suite,test, sharing tags across levels byref(test.module,test.suite, the ids, the source location).test.browser(Selenium) extendstest.test.gradle.session,test.gradle.module,test.maven.module,test.android(Robolectric),test.junit5, andjunit.callbackfor JUnit's before and after spans.optionaltags only, so the levels say which tags matter: what standard enrichment always sets (the CI provider and commit,ci.*/git.*) and the test source range arerecommended, and the tags set only some of the time stayoptional: pull request and head commit info, retries, ITR, test management, configuration errors, coverage, browser and Android details.Motivation
The id migration needs every tag registered, and the dense store gives a tag a precise presence bit only when it's placed on the span types it appears on.
Falling back to "unslotted" for the overflow would have been the easy way out, but every slotless tag shares bit 63: set on most spans, so checking any of them means a scan. A designed collision between tags that are rarely present together keeps the fast miss for the tags that matter, and deriving it from requirement levels keeps slots out of the conventions.
Additional Notes
base(_dd.span_links,_dd.svc_src,error.debug_info_captured,events). Every APM span type stays exact. The generator's output matches the earlier simulation exactly.slot:marker in the conventions (an earlier revision; slots are the generator's business, and the level already carries the information), two occupancy longs (8 bytes and a word select on every span, for CI's sake), or a group bit like trace-level (doesn't help: the overflow is within one span's tags).afterStartcopies theci.*/git.*block onto every session, module, suite and test span, though it's constant for the process. Setting it once per trace would be a CI Visibility behavior change (the intake may expect it on each event), for the CI Visibility team to decide; not changed here.test_session_id,test_module_id,test_suite_idanditr_correlation_idare set as tags and moved to top-level fields byCiTestCycleMapperV1, so they go throughsetTagand need ids. Theos.*/runtime.*fields are tags only when CI Visibility reports through the APM protocol, so they're shared.slot: sharedmarker is reverted by the later two.🤖 Generated with Claude Code