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>
This comment has been minimized.
This comment has been minimized.
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>
| * with its reason. | ||
| */ | ||
| class TagConstantsRegistryCoverageTest { | ||
| static final List<Class<?>> TAG_CONSTANT_CLASSES = |
There was a problem hiding this comment.
While I'll admit this test is a bit ugly, it acts as an explicitly sanity check that we're not missing anything. The exclusion list will get shorter in subsequent PRs.
🟢 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. |
…atterns 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>
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>
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>
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>
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>
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.
The two most actionable issues are: messaging span type declarations (producer, consumer, client) omit peer address/hostname/service tag mixins that RabbitMQ instrumentation already writes, and web.handler spans are excluded from the code-origin tag applies list, causing Spring controller spans to omit _dd.code_origin.type from their resolved tag sets.
🤖 Bits Code Review · Commit 20a0148
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>
sarahchen6
left a comment
There was a problem hiding this comment.
Changes look reasonable, though I'm not familiar with OTLP tag expectations TBH... Probably worth another check from the SDK Cap team
What Does This Do
Registers the APM tag constants in the tag registry, each on the span types it's actually set on, so instrumentation can migrate to setting tags by
KnownTags.*_IDand every tag gets a dense-store slot. A ratchet test tracks what's left.TagConstantsRegistryCoverageTestrequires everyStringconstant inTags,InstrumentationTagsandDDTagsto be registered, or listed by name or pattern (*):NEVER_REGISTEREDmaps each deliberate exception to its reason: values of a tag (Tags.SPAN_KIND_*), a name prefix, a name pattern, an operation name, names only the serializers write, and unused public API.NOT_YET_REGISTERED_FAMILIESnames whole families with a reason, today the CI Visibility constants (Tags.TEST_*,Tags.GIT_*,Tags.CI_*, ...). A family excuses only its unregistered constants, and is stale once none is left.NOT_YET_REGISTEREDlists single constants and can only shrink: registering one fails the test until its entry is removed. A new constant must be registered or listed.span_typesandmixins. Java-only tags (servlet's, Spring's, TIBCO's, Mule's) live intag-conventions-java.yaml, keeping the shared file language-agnostic. An overlay span type must extend a span type; an overlay mixin must say what itappliesto.servlet.pathandservlet.contextmove out of the shared file into a Javaservlet.requesttype.New span types
messaging.producer,.consumer,.broker(time in queue),.client(AMQP commands),.poll, withkafkaandamqpmixins.rpc.client,rpc.server.db.clientkeeps what any database client sets;db.sql(JDBC, R2DBC),db.cassandra,db.couchbase,db.redisanddb.mongoextend it. Database Monitoring's propagation tags are adbmmixin ondb.sqlanddb.mongo.aws.client(anhttp.client) and one type per service:aws.s3,.sqs,.sns,.kinesis,.dynamodb,.sfn,.lambda. Oneaws.clientholding every service's tags would be 67 tags, over the 63-slot cap, though a span never carries two services' tags.websocket.receive,.send,.close.web.handler(framework controller spans),internal(internal spans no other type models).servlet.request,spring.view.render,spring.webflux.handler,spring.webflux.client,tibco.process,mule.action,elasticsearch.rest,elasticsearch.transport,twilio.sdk,jsp.compile,jsp.render,quartz.job,hystrix.command.Motivation
The registry had 66 tags against about 350 tag constants. The migration to ids needs every tag registered, and the dense store gives a tag a precise slot only when it's on the span types it appears on.
Additional Notes
Tags.DB_HOSTandInstrumentationTags.JMS_PRODUCE(whose string isJMSDecorator's operation name).jsp.requestURL,elasticsearch.action, ...): tags set only by literal are outside this test's reach, and APMLP-2046 proposes auditing them in the instrumentation tests.elasticsearch.transport, which adds 21 tags todb.client.trace_levelholds both process-static facts (the.enabledflags, now including_dd.apm.enabled) and values common to a trace (local-root tags such as the attribute schema version)._dd.code_origin.frames.<N>.<field>has unboundedly many names, so it can't have ids ahead of time and can't use the dense store; its members stay custom tags. Frame 0's constants are listed as never registered for that reason, and only_dd.code_origin.typeis registered. A general registry could later describe patterns (name, type, OpenTelemetry naming) without ids.peer.hostnamefrom the handshake, butoutbound_peerdeclares it for client spans only; left as a TODO in the YAML.🤖 Generated with Claude Code