Skip to content

Scope OpenTelemetry tag names by span direction in the tag registry - #12713

Open
dougqh wants to merge 23 commits into
dougqh/tag-registry-otelfrom
dougqh/tag-registry-frames
Open

dougqh wants to merge 23 commits into
dougqh/tag-registry-otelfrom
dougqh/tag-registry-frames

Conversation

@dougqh

@dougqh dougqh commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

What Does This Do

Follow-on to #12354 (stacked on it; the diff here is only the follow-on). #12354 had to drop several OpenTelemetry renames because their meaning depends on span direction, and a flat rename cannot express that. This PR teaches the tag registry about direction. Phase 1 is registry-only. Every rename TagMap and OTLP apply today is unchanged; the one generated change is that peer.port becomes one tag per direction (see below).

The problem

OpenTelemetry names fall into two frames:

  • Absolute — a fixed role: server.address, server.port, client.address, client.port.
  • Relative — the other end of the connection: network.peer.address, network.peer.port.

Datadog mixes the two as well. http.hostname and http.client_ip are absolute, but peer.hostname, peer.ipv4/6, and peer.port are relative. HttpServerDecorator sets peer.port to the client's port, and the client decorators set it to the server's. So server.address is http.hostname on an inbound span and peer.hostname on an outbound one, and peer.port is client.port inbound but server.port outbound.

Same-frame mappings never need context. Mapping a relative name to an absolute one needs exactly one bit: the span's direction.

What changes (phase 1)

The conventions are arranged like an object model: span types are classes, mixins are traits. Direction now fits that model.

  • span-kind on span types and mixins (server|client|producer|consumer|internal, inherited through extends). It sets the direction: server/consumer are inbound, client/producer are outbound, internal has none. A directional mixin may only reach span types of its direction, and a span type may not change the direction it inherits through extends; the generator checks both, so no type can receive both sides of a tag declared per direction.

  • Where a rename is declared scopes it. In a scope without a span-kind (trace_level, an abstract type, a plain mixin) it applies in every direction; in a directional scope, only in that direction.

  • A tag whose meaning flips with direction is declared once per direction, each declaration in its own directional mixin. Each becomes its own tag, <dd-name>@<direction>, with fixed names in both namespaces:

    outbound_peer:              # the other end of an outbound connection: the server
      span-kind: client
      tags:
        - { dd-name: peer.hostname, otel-name: server.address }
        - { dd-name: peer.port,     otel-name: server.port }
    inbound_peer:               # the other end of an inbound connection: the client
      span-kind: server
      tags:
        - { dd-name: peer.port,     otel-name: client.port }

    peer.port@outbound and peer.port@inbound share the Datadog name, but each has one OTel name, so the reverse translation needs no context: input resolves to the specific tag, and output is a plain id→name lookup per namespace. Any other repeated declaration is still an error, and a ref to a shared name resolves by the referencing scope's direction.

  • Shared Datadog names are explicit. peer.port is one name for two tags. It isn't 1-to-1, so the mapping from name to tag needs context, while the mapping from tag to name doesn't. KnownTags generates no name constant for it, since a bare "peer.port" would only invite direction-blind use. Callers use the unambiguous IDs (PEER_PORT_OUTBOUND_ID, PEER_PORT_INBOUND_ID), each documented with its name and direction. tag-assignment.txt lists shared names:

    # SHARED DATADOG NAMES ...
      peer.port                      -> peer.port@inbound, peer.port@outbound
    
  • Names are unique per direction, in both namespaces. server.address is http.hostname inbound and peer.hostname outbound; peer.port is one tag per direction.

  • Only direction-free renames feed the generated tables, because TagMap and OTLP do not know a span's direction yet. Direction-scoped renames are validated and listed in tag-assignment.txt:

    # DIRECTION-SCOPED OPENTELEMETRY NAMES ...
      client.port                    inbound   -> peer.port@inbound
      network.peer.address           inbound   -> network.client.ip
      server.address                 inbound   -> http.hostname
      server.address                 outbound  -> peer.hostname
      server.port                    outbound  -> peer.port@outbound
    
  • The one interim runtime change: the bare peer.port names two tags, so the direction-free keyOf("peer.port") returns 0 (unknown) instead of guessing. Its output is unchanged: it had no direction-free rename, so it still exports as peer.port.

  • span-kind-neutral narrows to "apply this directional rename in every direction" (for example, db.system only ever names a database). It is still required on a concrete type with no span-kind, and is rejected on a tag declared per direction, whose meaning flips with direction by definition. Phase 2 removes it.

  • YAML: every concrete type declares a span-kind. peer.* is split into peer_address (peer.ipv4/6, shared), outbound_peer, and inbound_peer, so http.server now carries its client's peer.*, fixing Add the tag registry and map OpenTelemetry tag names through it (otel/otlp) #12354's client-only modeling.

Planned phases

  1. This PR, registry model: span directions, directional mixins, per-direction declarations and validation, and the YAML.
  2. Runtime resolution: writers resolve input to the specific tag using the span's direction, so TagMap stays direction-agnostic and serializers emit by id with no context (Set known tags by id on TagMap and spans #12715 adds id-keyed setters and makes TagMap.Entry carry its tag id). Where resolution happens is still open: in the core span (DDSpanContext) for every writer, or at each namespace's entry point (OTel names in the OTel API layer, Datadog names at instrumentation call sites). The direction would come from the span's span.kind or, for decorator-built spans, the SpanPrototype. The per-direction lookup is generated into KnownTags alongside that first consumer. This retires span-kind-neutral.
  3. Remaining mappings, after a semantic-conventions audit: network.peer.address for peer.ipv4/6, which splits by address family rather than direction, so the registry can't resolve it. The plan is a resolved-by: <layer> marker naming the layer responsible (e.g. the OTel API layer), which allows the mutually exclusive shared output name and keeps it out of every lookup table. Also: messaging consumers, where it needs checking whether server.address means the broker; and proxies, which may carry both frames on one span.

Phase 2 is a separate PR, not more commits here. It builds on this PR and on #12715, which adds setting known tags by id to TagMap and spans (a sibling of this PR, also stacked on #12354):

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

Motivation

Resolution belongs at the writer, which has the context to pick the right concept. Datadog instrumentation writes by Datadog name, the OTel bridge knows its span kind, and a serializer knows its target namespace. Each tag needs one identity and unique input names, while output names may collide across directions. dd-trace-dotnet reaches the same result with typed ClientTags/ServerTags classes; here the registry carries that context instead.

Additional Notes

  • New generator tests follow the suite's existing conventions. They cover typed renames scoped to their direction, one name mapping to different tags per direction, and span-kind-neutral widening, plus 10 invalid configurations.
  • Tag ids shift because peer.port becomes two tags; ids are not a stable contract. The direction-free rename table is identical to Add the tag registry and map OpenTelemetry tag names through it (otel/otlp) #12354's.

Generated report changes

The generated reports now live under internal-api/build/generated/tag-registry/ (regenerate with ./gradlew :internal-api:generateKnownTags), so they no longer show up in the PR diff. Here they are against #12354. Labels like peer.port@inbound are the generator's derived identities for a name declared per direction, not YAML syntax.

resolved-tags.txt: http.server gains its client's peer.*; peer.port becomes one tag per direction
--- resolved-tags.txt (#12354)
+++ resolved-tags.txt (this PR)
@@ -24,10 +24,10 @@
   - peer.service
   - _dd.peer.service.source
   - _dd.peer.service.remapped_from
-  - peer.hostname
   - peer.ipv4
   - peer.ipv6
-  - peer.port
+  - peer.hostname
+  - peer.port@outbound
 
 http.client  (21 tags):
   - _dd.parent_id
@@ -47,12 +47,12 @@
   - peer.service
   - _dd.peer.service.source
   - _dd.peer.service.remapped_from
-  - peer.hostname
   - peer.ipv4
   - peer.ipv6
-  - peer.port
+  - peer.hostname
+  - peer.port@outbound
 
-http.server  (21 tags):
+http.server  (24 tags):
   - _dd.parent_id
   - service
   - component
@@ -74,6 +74,9 @@
   - servlet.context
   - http.client_ip
   - network.client.ip
+  - peer.ipv4
+  - peer.ipv6
+  - peer.port@inbound
 
 view.render  (10 tags):
   - _dd.parent_id
tag-assignment.txt: peer.port splits in two (serials after it shift by one); new direction-scoped and shared-name sections
--- tag-assignment.txt (#12354)
+++ tag-assignment.txt (this PR)
@@ -1,4 +1,4 @@
-# Tag id assignment.  tags=53
+# Tag id assignment.  tags=54
 
 # TAGS     serial lvl id                 required     name
        1   T  0x0001000000000004 recommended  _dd.appsec.enabled
@@ -41,19 +41,20 @@
       38   -  0x0026000000000000 recommended  peer.hostname
       39   -  0x0027000000000000 optional     peer.ipv4
       40   -  0x0028000000000000 optional     peer.ipv6
-      41   -  0x0029000000000000 optional     peer.port
-      42   -  0x002A000000000000 recommended  peer.service
-      43   T  0x002B000000000004 required     runtime-id
-      44   -  0x002C000000000000 required     service
-      45   -  0x002D000000000000 optional     servlet.context
-      46   -  0x002E000000000000 optional     servlet.path
-      47   -  0x002F000000000000 required     span.kind
-      48   -  0x0030000000000000 recommended  test.framework
-      49   -  0x0031000000000000 recommended  test.name
-      50   -  0x0032000000000000 recommended  test.status
-      51   -  0x0033000000000000 recommended  test.suite
-      52   T  0x0034000000000004 recommended  version
-      53   -  0x0035000000000000 recommended  view.name
+      41   -  0x0029000000000000 optional     peer.port@inbound
+      42   -  0x002A000000000000 optional     peer.port@outbound
+      43   -  0x002B000000000000 recommended  peer.service
+      44   T  0x002C000000000004 required     runtime-id
+      45   -  0x002D000000000000 required     service
+      46   -  0x002E000000000000 optional     servlet.context
+      47   -  0x002F000000000000 optional     servlet.path
+      48   -  0x0030000000000000 required     span.kind
+      49   -  0x0031000000000000 recommended  test.framework
+      50   -  0x0032000000000000 recommended  test.name
+      51   -  0x0033000000000000 recommended  test.status
+      52   -  0x0034000000000000 recommended  test.suite
+      53   T  0x0035000000000004 recommended  version
+      54   -  0x0036000000000000 recommended  view.name
 
 # OPENTELEMETRY NAMES. keyOf(otelName) resolves to the canonical tag's id; nameOf still
 # returns the Datadog name, openTelemetryNameOf returns the name below. (No distinct id.)
@@ -66,3 +67,15 @@
   service.name                   -> service
   url.full                       -> http.url
   user_agent.original            -> http.useragent
+
+# DIRECTION-SCOPED OPENTELEMETRY NAMES. Each applies only on spans of the given direction, so
+# it is not in the tables above; name resolution does not use it until it knows the direction.
+  client.port                    inbound   -> peer.port@inbound
+  network.peer.address           inbound   -> network.client.ip
+  server.address                 inbound   -> http.hostname
+  server.address                 outbound  -> peer.hostname
+  server.port                    outbound  -> peer.port@outbound
+
+# SHARED DATADOG NAMES. One Datadog name for a tag per direction: emitting it needs no context,
+# but resolving the name to a tag needs the span's direction, so keyOf does not resolve it yet.
+  peer.port                      -> peer.port@inbound, peer.port@outbound

Contributor Checklist

  • build-logic :tag-registry:test (71 tests) and :tag-registry:spotlessCheck green
  • :internal-api:test (KnownTags*, TagMap*), :dd-trace-core:test (*otlp*, *taginterceptor*), OpenTelemetry14ConventionsTest green

Jira ticket

N/A

🤖 Generated with Claude Code

dougqh and others added 3 commits October 1, 2026 11:41
An OpenTelemetry name can mean different tags depending on direction:
server.address is the local host on an inbound span and the remote one on
an outbound span. Span types may now declare span-kind, which sets their
direction; a rename on such a type applies only in that direction. A
frame: relative mixin (tags naming the other end of the connection) can
give each direction its own name with otel-name: { outbound, inbound }.
OpenTelemetry names are now unique per direction rather than globally.

Name resolution is not direction-aware yet, so only renames that apply in
every direction feed the generated tables; direction-scoped ones are
validated and listed in tag-assignment.txt. span-kind-neutral now widens
a typed rename to every direction, and is still required on a concrete
type with no span-kind. Generated KnownTags is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds span-kind to every concrete span type and moves peer.hostname/ipv4/
ipv6/port into a frame: relative peer_endpoint mixin, now also included
by http.server (server spans carry peer.* for the client's address).
Declares the renames #12354 had to drop as direction-scoped:
server.address (http.hostname inbound, peer.hostname outbound),
server.port/client.port on peer.port, and network.peer.address
(network.client.ip, inbound). They are recorded but not yet applied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dougqh dougqh added comp: core Tracer core tag: no release notes Changes to exclude from release notes type: refactoring tag: ai generated Largely based on code generated by an AI or LLM labels Oct 1, 2026
@datadog-datadog-prod-us1

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.91 s 14.77 s [+0.2%; +1.7%] (maybe worse)
startup:insecure-bank:tracing:Agent 13.69 s 13.85 s [-2.1%; -0.2%] (maybe better)
startup:petclinic:appsec:Agent 17.23 s 16.96 s [+0.8%; +2.3%] (maybe worse)
startup:petclinic:iast:Agent 16.90 s 17.01 s [-1.4%; +0.1%] (no difference)
startup:petclinic:profiling:Agent 16.08 s 16.84 s [-8.7%; -0.3%] (maybe better)
startup:petclinic:sca:Agent 17.15 s 16.92 s [+0.4%; +2.2%] (maybe worse)
startup:petclinic:tracing:Agent 15.75 s 16.15 s [-6.4%; +1.4%] (no difference)

Commit: 73271adc · 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 and others added 8 commits October 1, 2026 13:52
Replaces frame: relative and the { outbound, inbound } otel-name map with
directional mixins. A mixin may declare span-kind; its renames then apply
only in that direction, and only span types of that direction may receive
it. A Datadog name may be declared once per direction: each declaration is
its own tag, <dd-name>@<direction>, with fixed names in both namespaces,
so export needs no context. A ref to such a name resolves by the
referencing scope's direction. A Datadog name shared this way is left out
of the direction-free keyOf table, since it names no single tag without a
direction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces the peer_endpoint mixin with peer_address (peer.ipv4/ipv6, shared)
and directional outbound_peer (peer.hostname -> server.address, peer.port
-> server.port) and inbound_peer (peer.port -> client.port) mixins.
peer.port becomes peer.port@outbound and peer.port@inbound; the bare name
no longer resolves to a single tag until resolution is direction-aware.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
peer.port names two tags, peer.port@outbound and peer.port@inbound. KnownTags
now emits one PEER_PORT_NAME constant for the shared name, alongside each
tag's own ID, instead of two NAME constants with the same value.
tag-assignment.txt lists shared Datadog names: emitting one needs no
context, but resolving it to a tag needs the span's direction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A shared name like peer.port names no single tag, so a String constant for
it would only invite direction-blind use. KnownTags now emits just the
unambiguous per-direction IDs, each documented with its name and
direction; nameOf returns the shared name as a literal. The registry tag
carries its declaring direction for that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dougqh dougqh mentioned this pull request Oct 1, 2026
4 tasks done
dougqh and others added 6 commits October 1, 2026 17:34
…tions

Says that <dd-name>@<direction> is the generator's report label, not YAML
syntax; notes that server spans also carry the client address in
peer.ipv4/ipv6, so mapping those inbound too would export the attribute
twice; and points the address-family TODO at the planned resolved-by
marker. Comments only; generated output is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Separates when to set span-kind-neutral from its interim behavior, states
that a type without a span-kind cannot receive a directional mixin, and
fixes two sentences whose reasoning read out of order. Comments only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SpanType and Mixin carried the raw span-kind string, and five call sites
looked it up again to get a Direction (one with !!). They now store the
Direction, parsed once. validateMixinDirections reuses appliedMixins
instead of re-implementing which mixins reach a type, and each span
type's direction is computed once while assigning identities. Generated
output is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The emitter and the assignment report each recomputed shared Datadog names
with groupBy { ddName }, though every tag already carries the answer in
sharedNameDirection. Use that. Generated output is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TagRegistry.build walked otelMappings twice: once for the per-direction
map and again inside TagConventions.directionFreeOtelNames. A rename that
covers every direction is now read off the per-direction map, and the
second method -- with its always-true distinct-names check -- is gone.
validateOtelNames also drops a prev == t.name escape that could never
hold, since each tag has one declaration. Generated output is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dougqh
dougqh marked this pull request as ready for review October 1, 2026 21:47
@dougqh
dougqh requested review from a team as code owners October 1, 2026 21:47
@dougqh
dougqh requested review from sarahchen6 and zacharycmontoya and removed request for a team October 1, 2026 21:47

@datadog-datadog-prod-us1 datadog-datadog-prod-us1 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 two most critical issues are: (1) inherited shared-name tags from a directional parent are not validated against a child's differing direction, allowing ambiguous resolved tag sets containing both inbound and outbound identities; and (2) shared-name aliases marked span-kind-neutral are incorrectly included in direction-free runtime tables, causing their resolved IDs to be lost and OTLP to emit the wrong tag name.

Open Bits AI session

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

private fun validateMixinDirections(spanTypes: Map<String, SpanType>, mixins: Map<String, Mixin>) {
for (st in spanTypes.values.filter { !it.abstract }) {
val chain = chainOf(spanTypes, st.name)
val reaching = chain.flatMap { it.include }.mapNotNull { mixins[it] } + appliedMixins(mixins, chain)

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.

P2 Validate inherited shared tags against the child's direction

An outbound parent referencing peer.port and an inbound child including inbound_peer pass validation, but resolve(child) returns both peer.port@outbound and peer.port@inbound. Inherited references are fixed to the parent's direction, while this validation checks only mixins. The generator therefore accepts an ambiguous resolved tag set. Validate inherited shared identities against the receiving direction after identity assignment, or reject incompatible direction overrides.

Was this helpful? React 👍 or 👎
🤖 Bits Code Review · @DataDog review to ask questions · Open Bits AI session

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same root cause as the thread at :306, fixed in dc8009a: a span type can no longer change the direction it inherits, so a parent's refs and a child's mixins always resolve in the same direction. Your scenario is now a test case.

traceLevel,
id = encode(serial, traceLevel),
otelName = t.otelName
otelName = directionFree[t.name],

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.

P2 Keep shared-name aliases out of direction-free tables

Marking a shared-name declaration span-kind-neutral promotes its alias into the runtime table. For example, server.port resolves to peer.port@outbound, but TagMap.Entry canonicalizes it to peer.port and subsequently resolves that shared name to zero. OTLP then emits peer.port instead of server.port. Exclude shared-name aliases from direction-free tables until entries retain their resolved IDs, or reject this flag combination.

Suggested change
otelName = directionFree[t.name],
otelName = if (t.sharedNameDirection == null) directionFree[t.name] else null,

Was this helpful? React 👍 or 👎
🤖 Bits Code Review · @DataDog review to ask questions · Open Bits AI session

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in e6be534 by rejecting the combination instead of leaving the alias out of the table. Declaring a tag per direction says its meaning flips with direction, and span-kind-neutral says it doesn't, so the generator now refuses that config. span-kind-neutral itself goes away in phase 2, once resolution knows the span's direction.

* The OpenTelemetry name per span direction. A superset of [otelName]: a rename scoped to one
* direction appears only here, and is not applied until name resolution knows the direction.
*/
val otelByDirection: Map<TagConventions.Direction, String> = emptyMap(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Can we simplify this from a map to a singular TagConventions.Direction? ? Either this tag will be for one direction or all, so this can be represented by a nullable value:

Suggested change
val otelByDirection: Map<TagConventions.Direction, String> = emptyMap(),
val otelByDirection: TagConventions.Direction? = null,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, done in 4fb37ef. A tag has one declaration, so its rename covers every direction or exactly one; it's now otelDirection: Direction? (null = every direction), and the direction-free otelName is derived from that rather than from the map's size. Generated output is unchanged.

require(refList(traceLevelRaw).isEmpty()) { "trace_level tags must be declarations, not refs" }
validateSingleDeclaration(spanTypes, mixins, traceLevel)

validateMixinDirections(parsedSpanTypes, parsedMixins)

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 revealed to me that while we do validate mixin directions, inheritance is not validated. We could theoretically have an abstract parent declare span-kind: client while a subtype declares span-kind: server and the system would not throw an error

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's a good catch. Since I'm assuming we're happy with this approach to solving the naming ambiguities, I went ahead and added the enforcement in dc8009a.

One nice aspect of the tag registry is that it makes implementing these checks easy. That wasn't something I set out to do, but I agree with adding the checks.

"consumer" to Direction.INBOUND,
"client" to Direction.OUTBOUND,
"producer" to Direction.OUTBOUND,
"internal" to Direction.NONE,

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 asserts that a rename on an internal type is silently dropped. For example, if view.render with span-kind: internal declares {dd-name: view.name, otel-name: something}, then otelMappings().add generates a List<OtelMapping with size 1, then in TagRegistry.build() it is filtered out because 1 != TagConventions.Direction.entries.size. This means that no corresponding _OTEL_NAME is generated for this entry

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not dropped, deferred: internal is a directional scope (NONE), so the rename is scoped to direction-less spans and listed under DIRECTION-SCOPED in tag-assignment.txt, like any scoped rename in phase 1. Treating it as direction-free would also claim the OTel name on inbound and outbound spans. Since 4fb37ef that's explicit (otelDirection = NONE) rather than a side effect of the size filter, and phase 2 applies scoped renames once resolution knows the direction. Fair point on the "silent" part though — 59a6a2a calls out internal in the span-kind-neutral docs and the YAML header.

dougqh and others added 6 commits October 1, 2026 20:45
A child type's own span-kind must agree with the nearest ancestor's.
Otherwise the ancestor's refs resolve in the ancestor's direction while
the child's mixins apply in its own, so one type could receive both
sides of a tag declared per direction (e.g. peer.port@outbound and
peer.port@inbound).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Declaring a tag per direction says its meaning flips with direction;
span-kind-neutral says it doesn't. Accepting both put the alias into the
direction-free table, where TagMap canonicalizes it to the shared
Datadog name, which keyOf resolves to 0, so OTLP emitted the shared
name instead of the OpenTelemetry one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tag has one declaration, so its OpenTelemetry rename applies in every
direction or in exactly one. The per-direction map could express states
the model rules out; a nullable Direction (null = every direction) says
exactly what is possible. The direction-free otelName is now derived
from it rather than from the map's size. Generated output is unchanged.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ougqh/tag-registry-frames

# Conflicts:
#	build-logic/tag-registry/src/main/kotlin/datadog/buildlogic/tagRegistry/TagConventions.kt

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants