diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index 9281c377afb..1e236d3a120 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -1403,7 +1403,45 @@ Object getTag(final String key) { * @return the value associated with the tag */ public Object unsafeGetTag(final String tag) { - return unsafeTags.getObject(tag); + Object value = unsafeTags.getObject(tag); + return value != null ? value : unsafeGetDirectionScopedTag(tag); + } + + /** + * Resolves an OpenTelemetry name whose tag depends on the span's direction, by this span's kind: + * {@code server.address} is {@code http.hostname} on inbound spans and {@code peer.hostname} on + * outbound ones. Tried only after a plain lookup misses, so a tag set under the literal name + * wins; null when the name isn't direction-scoped or the span has no direction. + */ + private Object unsafeGetDirectionScopedTag(final String tag) { + int direction = directionOfKind(spanKindOrdinal); + if (direction == KnownTagCodec.DIRECTION_UNKNOWN) { + return null; + } + long tagId = KnownTagCodec.directionalKeyOf(tag, direction); + if (tagId == 0) { + return null; + } + TagMap.Entry entry = unsafeTags.getEntry(tagId); + return entry == null ? null : entry.objectValue(); + } + + /** + * The {@link KnownTagCodec} direction of a span-kind ordinal; brokers and custom kinds have none. + */ + static int directionOfKind(byte spanKindOrdinal) { + switch (spanKindOrdinal) { + case SPAN_KIND_SERVER: + case SPAN_KIND_CONSUMER: + return KnownTagCodec.DIRECTION_INBOUND; + case SPAN_KIND_CLIENT: + case SPAN_KIND_PRODUCER: + return KnownTagCodec.DIRECTION_OUTBOUND; + case SPAN_KIND_INTERNAL: + return KnownTagCodec.DIRECTION_NONE; + default: + return KnownTagCodec.DIRECTION_UNKNOWN; + } } @Deprecated diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanContextTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanContextTest.java index 030687c06ec..221d350f032 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanContextTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanContextTest.java @@ -20,10 +20,12 @@ import static datadog.trace.core.DDSpanContext.SPAN_SAMPLING_MAX_PER_SECOND_TAG; import static datadog.trace.core.DDSpanContext.SPAN_SAMPLING_MECHANISM_TAG; import static datadog.trace.core.DDSpanContext.SPAN_SAMPLING_RULE_RATE_TAG; +import static java.util.Collections.singletonMap; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -40,6 +42,7 @@ import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.common.writer.ListWriter; import datadog.trace.core.propagation.ExtractedContext; +import datadog.trace.core.util.TagsMatcher; import datadog.trace.test.junit.utils.tabletest.TableTestTypeConverters; import java.util.HashMap; import java.util.Map; @@ -580,4 +583,68 @@ static void assertTagmap(Map source, Map comparison, boolean removeT private static String dataTag(String tag) { return "_dd." + tag + ".json"; } + + @TableTest({ + "scenario | kind | storedTag | readTag ", + "server.address on a server span | server | http.hostname | server.address ", + "server.address on a consumer span | consumer | http.hostname | server.address ", + "server.address on a client span | client | peer.hostname | server.address ", + "server.address on a producer span | producer | peer.hostname | server.address ", + "network.peer.address inbound | server | network.client.ip | network.peer.address" + }) + void readsDirectionScopedNameBySpanKind(String kind, String storedTag, String readTag) { + AgentSpan span = tracer.buildSpan("datadog", "fakeOperation").withTag(SPAN_KIND, kind).start(); + span.setTag(storedTag, "example.com"); + DDSpanContext context = (DDSpanContext) span.spanContext(); + + assertEquals("example.com", span.getTag(readTag)); + assertEquals("example.com", context.unsafeGetTag(readTag)); + span.finish(); + } + + @TableTest({ + "scenario | kind | storedTag ", + "outbound-only rename, server span | server | peer.hostname", + "inbound-only rename, client span | client | http.hostname", + "internal span | internal | http.hostname", + "broker span | broker | peer.hostname" + }) + void leavesDirectionScopedNameUnresolvedWhereItNamesNoTag(String kind, String storedTag) { + AgentSpan span = tracer.buildSpan("datadog", "fakeOperation").withTag(SPAN_KIND, kind).start(); + span.setTag(storedTag, "example.com"); + + assertNull(span.getTag("server.address")); + span.finish(); + } + + @Test + void leavesDirectionScopedNameUnresolvedWithoutSpanKind() { + AgentSpan span = tracer.buildSpan("datadog", "fakeOperation").start(); + span.setTag("http.hostname", "example.com"); + span.setTag("peer.hostname", "example.com"); + + assertNull(span.getTag("server.address")); + span.finish(); + } + + @Test + void prefersTagSetUnderTheLiteralDirectionScopedName() { + AgentSpan span = + tracer.buildSpan("datadog", "fakeOperation").withTag(SPAN_KIND, "client").start(); + span.setTag("peer.hostname", "resolved.example.com"); + span.setTag("server.address", "literal.example.com"); + + assertEquals("literal.example.com", span.getTag("server.address")); + span.finish(); + } + + @Test + void samplingRuleMatchesDirectionScopedName() { + AgentSpan span = + tracer.buildSpan("datadog", "fakeOperation").withTag(SPAN_KIND, "client").start(); + span.setTag("peer.hostname", "db.example.com"); + + assertTrue(TagsMatcher.create(singletonMap("server.address", "db.*")).matches((DDSpan) span)); + span.finish(); + } }