diff --git a/dd-trace-core/build.gradle b/dd-trace-core/build.gradle index ee8ef78ee16..ab35cd77bc1 100644 --- a/dd-trace-core/build.gradle +++ b/dd-trace-core/build.gradle @@ -122,6 +122,18 @@ jmh { if (project.hasProperty('jmh.profilers')) { profilers = project.property('jmh.profilers').tokenize(',') } + if (project.hasProperty('jmh.jvmArgs')) { + jvmArgsAppend = project.property('jmh.jvmArgs').tokenize(' ') + } + if (project.hasProperty('jmh.fork')) { + fork = project.property('jmh.fork') as Integer + } + if (project.hasProperty('jmh.warmupIterations')) { + warmupIterations = project.property('jmh.warmupIterations') as Integer + } + if (project.hasProperty('jmh.iterations')) { + iterations = project.property('jmh.iterations') as Integer + } if (project.hasProperty('testJvm')) { def testJvmSpec = new TestJvmSpec(project) jvm = testJvmSpec.javaTestLauncher.map { it.executablePath.asFile.absolutePath } diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/DDSpanContextSetTagBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/DDSpanContextSetTagBenchmark.java index 2ee2ac079e9..b038d90621b 100644 --- a/dd-trace-core/src/jmh/java/datadog/trace/core/DDSpanContextSetTagBenchmark.java +++ b/dd-trace-core/src/jmh/java/datadog/trace/core/DDSpanContextSetTagBenchmark.java @@ -1,5 +1,6 @@ package datadog.trace.core; +import datadog.trace.api.DDTags; import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.common.writer.Writer; import java.util.List; @@ -39,9 +40,52 @@ public class DDSpanContextSetTagBenchmark { private static final CoreTracer TRACER = CoreTracer.builder().writer(new NoopWriter()).strictTraceWrites(false).build(); + // A realistic mix set on one span: ~1/3 intercepted (resource/status/kind/error/service), ~2/3 + // ordinary app/integration tags. Cycling these through a single setTag call site keeps the + // handlerId/handleIntercept dispatch polymorphic and interleaves the store -- closer to + // production + // than the single-arm benchmarks, and it should keep C2 out of the degenerate single-mode that + // the + // notIntercepted-only loop locks into. setTag(String, Object) is the manual-instrumentation path. + private static final String[] MIXED_TAGS = { + DDTags.RESOURCE_NAME, + "http.useragent", + "db.instance", + Tags.HTTP_STATUS, + "component", + "thread.name", + Tags.SPAN_KIND, + "messaging.system", + "network.peer.address", + Tags.ERROR, + "http.route", + DDTags.SERVICE_NAME, + "rpc.method", + "app.queue.depth", + "http.request.content_length" + }; + private static final Object[] MIXED_VALUES = { + "GET /api/users", + "Mozilla/5.0", + "orders", + 200, + "netty", + "worker-1", + "server", + "kafka", + "10.0.0.1", + Boolean.FALSE, + "/api/users/{id}", + "my-service", + "GetUser", + 42, + 1024 + }; + @State(Scope.Thread) public static class SpanState { DDSpan span; + int mixIdx; @Setup public void setup() { @@ -60,6 +104,14 @@ public Object notIntercepted(SpanState s) { return s.span.setTag("app.queue.depth", 200); } + /** Realistic mix of intercepted + non-intercepted tags cycled through one setTag call site. */ + @Benchmark + public Object mixed(SpanState s) { + int i = s.mixIdx; + s.mixIdx = (i + 1 == MIXED_TAGS.length) ? 0 : i + 1; + return s.span.setTag(MIXED_TAGS[i], MIXED_VALUES[i]); + } + static final class NoopWriter implements Writer { @Override public void write(List trace) {} 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 6cb3927373f..9bffd4d18f7 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 @@ -34,6 +34,7 @@ import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import datadog.trace.core.propagation.PropagationTags; import datadog.trace.core.taginterceptor.TagInterceptor; +import datadog.trace.core.taginterceptor.TagInterceptor.TagHandler; import datadog.trace.core.tagprocessor.TagsPostProcessorFactory; import datadog.trace.util.TagsHelper; import java.io.Closeable; @@ -881,7 +882,13 @@ public void setTag(final String tag, final Object value) { } if (null == value) { removeTag(tag); - } else if (!tagInterceptor.interceptTag(this, tag, value)) { + return; + } + // Dispatch inline (not via interceptTag) so this overload owns its handle() call site: each + // caller that inlines setTag gets its own type profile, letting a tag-stable caller + // devirtualize and inline its one handler. + TagHandler handler = tagInterceptor.handler(tag); + if (handler == null || !handler.handle(this, tag, value)) { synchronized (unsafeTags) { unsafeTags.set(tag, value); } @@ -894,7 +901,10 @@ public void setTag(final String tag, final String value) { } if (null == value) { removeTag(tag); - } else if (!tagInterceptor.interceptTag(this, tag, value)) { + return; + } + TagHandler handler = tagInterceptor.handler(tag); + if (handler == null || !handler.handle(this, tag, value)) { synchronized (unsafeTags) { unsafeTags.set(tag, value); } @@ -906,11 +916,9 @@ public void setTag(TagMap.EntryReader entry) { return; } - // resolve the handler once; id 0 short-circuits without a second lookup - int handlerId = tagInterceptor.handlerId(entry.tag()); - boolean intercepted = - handlerId != 0 - && tagInterceptor.handleIntercept(this, handlerId, entry.tag(), entry.objectValue()); + // resolve the handler once; a miss (null) short-circuits without a second lookup + TagHandler handler = tagInterceptor.handler(entry.tag()); + boolean intercepted = handler != null && handler.handle(this, entry.tag(), entry.objectValue()); if (!intercepted) { synchronized (unsafeTags) { unsafeTags.set(entry); @@ -918,25 +926,19 @@ public void setTag(TagMap.EntryReader entry) { } } - /* - * Used when handlerId determined the tag is intercepted (id != 0): the primitive must be boxed to - * pass to the interceptor. The box is reused -- the optimized TagMap caches it on store. - */ - private void setBox(int handlerId, String tag, Object box) { - if (!tagInterceptor.handleIntercept(this, handlerId, tag, box)) { - synchronized (unsafeTags) { - unsafeTags.set(tag, box); - } - } - } - public void setTag(final String tag, final boolean value) { if (null == tag) { return; } - int handlerId = tagInterceptor.handlerId(tag); - if (handlerId != 0) { - this.setBox(handlerId, tag, value); + TagHandler handler = tagInterceptor.handler(tag); + if (handler != null) { + // intercepted: box once (the optimized TagMap caches it on store) and dispatch + Object box = value; + if (!handler.handle(this, tag, box)) { + synchronized (unsafeTags) { + unsafeTags.set(tag, box); + } + } } else { synchronized (unsafeTags) { unsafeTags.set(tag, value); @@ -948,9 +950,14 @@ public void setTag(final String tag, final int value) { if (null == tag) { return; } - int handlerId = tagInterceptor.handlerId(tag); - if (handlerId != 0) { - this.setBox(handlerId, tag, value); + TagHandler handler = tagInterceptor.handler(tag); + if (handler != null) { + Object box = value; + if (!handler.handle(this, tag, box)) { + synchronized (unsafeTags) { + unsafeTags.set(tag, box); + } + } } else { synchronized (unsafeTags) { unsafeTags.set(tag, value); @@ -962,10 +969,15 @@ public void setTag(final String tag, final long value) { if (null == tag) { return; } - // resolve once; handlerId 0 (not intercepted) stores the primitive without boxing - int handlerId = tagInterceptor.handlerId(tag); - if (handlerId != 0) { - this.setBox(handlerId, tag, value); + // resolve once; a miss (null) stores the primitive without boxing + TagHandler handler = tagInterceptor.handler(tag); + if (handler != null) { + Object box = value; + if (!handler.handle(this, tag, box)) { + synchronized (unsafeTags) { + unsafeTags.set(tag, box); + } + } } else { synchronized (unsafeTags) { unsafeTags.set(tag, value); @@ -977,9 +989,14 @@ public void setTag(final String tag, final float value) { if (null == tag) { return; } - int handlerId = tagInterceptor.handlerId(tag); - if (handlerId != 0) { - this.setBox(handlerId, tag, value); + TagHandler handler = tagInterceptor.handler(tag); + if (handler != null) { + Object box = value; + if (!handler.handle(this, tag, box)) { + synchronized (unsafeTags) { + unsafeTags.set(tag, box); + } + } } else { synchronized (unsafeTags) { unsafeTags.set(tag, value); @@ -991,9 +1008,14 @@ public void setTag(final String tag, final double value) { if (null == tag) { return; } - int handlerId = tagInterceptor.handlerId(tag); - if (handlerId != 0) { - this.setBox(handlerId, tag, value); + TagHandler handler = tagInterceptor.handler(tag); + if (handler != null) { + Object box = value; + if (!handler.handle(this, tag, box)) { + synchronized (unsafeTags) { + unsafeTags.set(tag, box); + } + } } else { synchronized (unsafeTags) { unsafeTags.set(tag, value); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java index 850002f234a..c83a7f19588 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/taginterceptor/TagInterceptor.java @@ -47,38 +47,39 @@ public final class TagInterceptor { private static final UTF8BytesString NOT_FOUND_RESOURCE_NAME = UTF8BytesString.create("404"); - // Handler ids for the intercept dispatch. 1-based: 0 is the skip sentinel ("not intercepted"), - // which lines up with the empty StringIndex slots that mapIntValues leaves at 0. - private static final int ID_RESOURCE_NAME = 1; - private static final int ID_DB_STATEMENT = 2; - private static final int ID_SERVICE = 3; // SERVICE_NAME and the legacy "service" - private static final int ID_PEER_SERVICE = 4; - private static final int ID_MANUAL_KEEP = 5; - private static final int ID_MANUAL_DROP = 6; - private static final int ID_ASM_KEEP = 7; - private static final int ID_AI_GUARD_KEEP = 8; - private static final int ID_SAMPLING_PRIORITY = 9; - private static final int ID_PROPAGATED_TRACE_SOURCE = 10; - private static final int ID_PROPAGATED_DEBUG = 11; - private static final int ID_SERVLET_CONTEXT = 12; - private static final int ID_SPAN_TYPE = 13; - private static final int ID_ANALYTICS_SAMPLE_RATE = 14; - private static final int ID_ERROR = 15; - private static final int ID_HTTP_STATUS = 16; - private static final int ID_URL_RESOURCE = 17; // HTTP_METHOD and HTTP_URL - private static final int ID_ORIGIN = 18; - private static final int ID_MEASURED = 19; - private static final int ID_SPAN_KIND = 20; - private static final int ID_SPLIT_SERVICE = 21; // a Config split-by-tag (not a fixed case) - - // Membership/dispatch index over the compile-time-fixed intercepted tags, held as the raw placed - // arrays (not a StringIndex instance) so handlerId's hot-path Support.lookup folds the refs to - // constants -- the path StringIndex recommends for hot code. Split-by-tags are Config-driven, so - // they stay a per-instance check (see handlerId) and never enter this index. + /** + * One intercept behavior, in a uniform shape so every case can live behind a single + * (devirtualizable) call site. Each {@code switch} arm of the old dispatch is now a method + * reference stored slot-aligned in {@link #handlers}; dispatch is an array load + this call rather + * than a 344-byte {@code tableswitch} that overran C2's inline budget. + */ + @FunctionalInterface + public interface TagHandler { + boolean handle(DDSpanContext span, String tag, Object value); + } + + private final RuleFlags ruleFlags; + private final boolean isServiceNameSetByUser; + private final boolean splitByServletContext; + private final String inferredServiceName; + // Config split-by-tags as a per-instance StringIndex (membership only; all map to SPLIT_SERVICE). + // Config-driven, so it can't be the folded static gate -- but it's the rare path (most deploys + // have none), gated by hasSplitTags, and checked only on a fixed-index miss. + private final StringIndex splitIndex; + + private final boolean shouldSet404ResourceName; + private final boolean shouldSetUrlResourceAsName; + private final boolean jeeSplitByDeployment; + + private final boolean hasSplitTags; // short-circuits the split check when none are configured + + // The compile-time-fixed intercepted tags, held as the raw placed arrays in STATIC FINAL fields + // so handler()'s gate -- Support.indexOf over these -- folds the refs to constants (the hot path + // StringIndex recommends; a per-instance index instead costs a field-load + loses the fold, which + // measured ~40% slower on the monomorphic intercepted path). Split-by-tags are Config-driven, so + // they stay a per-instance check in handler() on a fixed miss and never enter this index. private static final int[] FIXED_HASHES; private static final String[] FIXED_NAMES; - // Slot-aligned handler ids (built once). The name->id switch runs only at class init. - private static final int[] FIXED_HANDLER_IDS; static { StringIndex.Data fixed = @@ -107,69 +108,15 @@ public final class TagInterceptor { Tags.SPAN_KIND); FIXED_HASHES = fixed.hashes; FIXED_NAMES = fixed.names; - FIXED_HANDLER_IDS = - StringIndex.Support.mapIntValues(FIXED_NAMES, TagInterceptor::fixedHandlerId); } - private static int fixedHandlerId(String tag) { - switch (tag) { - case DDTags.RESOURCE_NAME: - return ID_RESOURCE_NAME; - case Tags.DB_STATEMENT: - return ID_DB_STATEMENT; - case DDTags.SERVICE_NAME: - case "service": - return ID_SERVICE; - case Tags.PEER_SERVICE: - return ID_PEER_SERVICE; - case DDTags.MANUAL_KEEP: - return ID_MANUAL_KEEP; - case DDTags.MANUAL_DROP: - return ID_MANUAL_DROP; - case Tags.ASM_KEEP: - return ID_ASM_KEEP; - case Tags.AI_GUARD_KEEP: - return ID_AI_GUARD_KEEP; - case Tags.SAMPLING_PRIORITY: - return ID_SAMPLING_PRIORITY; - case Tags.PROPAGATED_TRACE_SOURCE: - return ID_PROPAGATED_TRACE_SOURCE; - case Tags.PROPAGATED_DEBUG: - return ID_PROPAGATED_DEBUG; - case SERVLET_CONTEXT: - return ID_SERVLET_CONTEXT; - case SPAN_TYPE: - return ID_SPAN_TYPE; - case ANALYTICS_SAMPLE_RATE: - return ID_ANALYTICS_SAMPLE_RATE; - case Tags.ERROR: - return ID_ERROR; - case HTTP_STATUS: - return ID_HTTP_STATUS; - case HTTP_METHOD: - case HTTP_URL: - return ID_URL_RESOURCE; - case ORIGIN_KEY: - return ID_ORIGIN; - case MEASURED: - return ID_MEASURED; - case Tags.SPAN_KIND: - return ID_SPAN_KIND; - default: - return 0; // unreachable: FIXED only holds the names above - } - } + /** Singleton for Config split-by-tag service tags; returned by handler() on a fixed-index miss. */ + private static final TagHandler SPLIT_SERVICE = TagInterceptor::interceptSplitService; - private final RuleFlags ruleFlags; - private final boolean isServiceNameSetByUser; - private final boolean splitByServletContext; - private final String inferredServiceName; - private final Set splitServiceTags; - private final boolean hasSplitTags; // short-circuits the split check when none are configured - - private final boolean shouldSet404ResourceName; - private final boolean shouldSetUrlResourceAsName; - private final boolean jeeSplitByDeployment; + // Slot-aligned to FIXED_NAMES: handlers[indexOf(tag)] is tag's handler. Per-instance because some + // handler method refs close over config (ruleFlags, shouldSet404ResourceName, ...); the *lookup* + // over FIXED_HASHES/FIXED_NAMES still const-folds, only handlers[slot] is an instance array load. + private final TagHandler[] handlers; public TagInterceptor(RuleFlags ruleFlags) { this( @@ -188,8 +135,7 @@ public TagInterceptor( boolean jeeSplitByDeployment) { this.isServiceNameSetByUser = isServiceNameSetByUser; this.inferredServiceName = inferredServiceName; - this.splitServiceTags = splitServiceTags; - this.hasSplitTags = !splitServiceTags.isEmpty(); + this.splitIndex = StringIndex.of(splitServiceTags); this.ruleFlags = ruleFlags; splitByServletContext = splitServiceTags.contains(SERVLET_CONTEXT); @@ -199,6 +145,70 @@ public TagInterceptor( && ruleFlags.isEnabled(STATUS_404_DECORATOR); shouldSetUrlResourceAsName = ruleFlags.isEnabled(URL_AS_RESOURCE_NAME); this.jeeSplitByDeployment = jeeSplitByDeployment; + + this.hasSplitTags = !splitServiceTags.isEmpty(); + + // Build the slot-aligned handler array over the static FIXED_NAMES placement. Handlers keep + // their own ruleFlags checks (behavior-identical to the old switch). Split-by-tags are NOT in + // this array -- handler() checks them per-instance on a fixed miss, preserving "fixed wins". + this.handlers = + StringIndex.Support.mapValues(FIXED_NAMES, TagHandler.class, this::fixedHandler); + } + + /** The handler for a compile-time-fixed tag (called once per slot at construction). */ + private TagHandler fixedHandler(String tag) { + switch (tag) { + case DDTags.RESOURCE_NAME: + // config-hoist: install only if enabled, so the handler body drops the per-call check + // (and stays static). Disabled -> null slot -> handler() returns null -> tag is stored. + return ruleFlags.isEnabled(RESOURCE_NAME) ? TagInterceptor::interceptResourceName : null; + case Tags.DB_STATEMENT: + return TagInterceptor::interceptDbStatement; + case DDTags.SERVICE_NAME: + case "service": + return ruleFlags.isEnabled(SERVICE_NAME) ? TagInterceptor::interceptServiceName : null; + case Tags.PEER_SERVICE: + // not hoisted: it sets PEER_SERVICE_SOURCE even when PEER_SERVICE is disabled, so the + // enabled check must stay inside the handler (and it stays an instance ref). + return this::interceptPeerService; + case DDTags.MANUAL_KEEP: + return TagInterceptor::interceptManualKeep; + case DDTags.MANUAL_DROP: + return ruleFlags.isEnabled(FORCE_MANUAL_DROP) ? TagInterceptor::interceptManualDrop : null; + case Tags.ASM_KEEP: + return TagInterceptor::interceptAsmKeep; + case Tags.AI_GUARD_KEEP: + return TagInterceptor::interceptAiGuardKeep; + case Tags.SAMPLING_PRIORITY: + return ruleFlags.isEnabled(FORCE_SAMPLING_PRIORITY) + ? TagInterceptor::interceptSamplingPriority + : null; + case Tags.PROPAGATED_TRACE_SOURCE: + return TagInterceptor::interceptPropagatedTraceSource; + case Tags.PROPAGATED_DEBUG: + return TagInterceptor::interceptPropagatedDebug; + case SERVLET_CONTEXT: + return this::interceptServletContext; + case SPAN_TYPE: + return TagInterceptor::interceptSpanType; + case ANALYTICS_SAMPLE_RATE: + return TagInterceptor::interceptAnalyticsSampleRate; + case Tags.ERROR: + return TagInterceptor::interceptError; + case HTTP_STATUS: + return this::interceptHttpStatusCode; + case HTTP_METHOD: + case HTTP_URL: + return shouldSetUrlResourceAsName ? TagInterceptor::interceptUrlResourceAsNameRule : null; + case ORIGIN_KEY: + return TagInterceptor::interceptOrigin; + case MEASURED: + return TagInterceptor::interceptMeasured; + case Tags.SPAN_KIND: + return TagInterceptor::interceptSpanKind; + default: + return null; // unreachable: FIXED_NAMES only holds the names above + } } public boolean needsIntercept(TagMap map) { @@ -216,118 +226,44 @@ public boolean needsIntercept(Map map) { } public boolean needsIntercept(String tag) { - return handlerId(tag) != 0; + return handler(tag) != null; } /** - * Resolves {@code tag} to a dispatch handler id ({@code ID_*}), or {@code 0} when not - * intercepted. Fixed (compile-time) tags come from the static {@code FIXED_*} index; a Config - * split-by-tag -- checked only on a fixed miss -- resolves to {@link #ID_SPLIT_SERVICE}. Fixed - * wins when a tag is both, matching the original switch order. + * Resolves {@code tag} to its {@link TagHandler}, or {@code null} when not intercepted. The gate + * is a {@code Support.indexOf} over the {@code static final} {@link #FIXED_HASHES}/{@link + * #FIXED_NAMES} -- C2 folds those refs to constants, so the resolve is near-free; a Config + * split-by-tag is checked per-instance only on a fixed miss (fixed wins, matching the original + * order). Returning the handler (rather than an int id routed back through a shared {@code + * handleIntercept}) lets each caller invoke {@code handler.handle(...)} at its own site, so each + * call site gets an independent type profile: a tag-stable caller can devirtualize and inline its + * one handler. {@code null} is the "store, don't intercept" sentinel callers use to skip boxing. */ - public int handlerId(String tag) { - // lookup returns the slot's id, or 0 on a miss -- which is exactly the "not fixed" sentinel - // (ids are 1-based), so no separate slot >= 0 check is needed. - int id = StringIndex.Support.lookup(FIXED_HASHES, FIXED_NAMES, FIXED_HANDLER_IDS, tag); - if (id != 0) { - return id; + public TagHandler handler(String tag) { + int slot = StringIndex.Support.indexOf(FIXED_HASHES, FIXED_NAMES, tag); + if (slot >= 0) { + return handlers[slot]; } - return hasSplitTags && splitServiceTags.contains(tag) ? ID_SPLIT_SERVICE : 0; + return hasSplitTags && splitIndex.contains(tag) ? SPLIT_SERVICE : null; } - /** - * Convenience: resolve the handler id and dispatch. {@code id == 0} short-circuits (not ours). - */ + /** Convenience: resolve the handler and dispatch. A miss short-circuits (not ours). */ public boolean interceptTag(DDSpanContext span, String tag, Object value) { - int handlerId = handlerId(tag); - return handlerId != 0 && handleIntercept(span, handlerId, tag, value); - } - - public boolean handleIntercept(DDSpanContext span, int handlerId, String tag, Object value) { - switch (handlerId) { - case ID_RESOURCE_NAME: - return interceptResourceName(span, value); - case ID_DB_STATEMENT: - return interceptDbStatement(span, value); - case ID_SERVICE: - return interceptServiceName(SERVICE_NAME, span, value); - case ID_PEER_SERVICE: - // we still need to intercept and add this tag when the user manually set - span.setTag(DDTags.PEER_SERVICE_SOURCE, Tags.PEER_SERVICE); - return interceptServiceName(PEER_SERVICE, span, value); - case ID_MANUAL_KEEP: - if (asBoolean(value)) { - span.forceKeep(); - return true; - } - return false; - case ID_MANUAL_DROP: - return interceptSamplingPriority( - FORCE_MANUAL_DROP, USER_DROP, SamplingMechanism.MANUAL, span, value); - case ID_ASM_KEEP: - if (asBoolean(value)) { - span.forceKeep(SamplingMechanism.APPSEC); - return true; - } - return false; - case ID_AI_GUARD_KEEP: - if (asBoolean(value)) { - span.forceKeep(SamplingMechanism.AI_GUARD); - return true; - } - return false; - case ID_SAMPLING_PRIORITY: - return interceptSamplingPriority(span, value); - case ID_PROPAGATED_TRACE_SOURCE: - if (value instanceof Integer) { - span.addPropagatedTraceSource((Integer) value); - return true; - } - return false; - case ID_PROPAGATED_DEBUG: - span.updateDebugPropagation(String.valueOf(value)); - return true; - case ID_SERVLET_CONTEXT: - return interceptServletContext(span, value); - case ID_SPAN_TYPE: - return interceptSpanType(span, value); - case ID_ANALYTICS_SAMPLE_RATE: - return interceptAnalyticsSampleRate(span, value); - case ID_ERROR: - return interceptError(span, value); - case ID_HTTP_STATUS: - // not set internally but may come from manual instrumentation - return interceptHttpStatusCode(span, value); - case ID_URL_RESOURCE: - return interceptUrlResourceAsNameRule(span, tag, value); - case ID_ORIGIN: - return interceptOrigin(span, value); - case ID_MEASURED: - return interceptMeasured(span, value); - case ID_SPAN_KIND: - // Cache the ordinal for fast isOutbound() checks. - // Return false so the value is still stored in unsafeTags for serialization. - span.setSpanKindOrdinal(String.valueOf(value)); - return false; - case ID_SPLIT_SERVICE: - span.setServiceName(String.valueOf(value), SPLIT_BY_TAGS); - return true; - default: - return false; - } + TagHandler handler = handler(tag); + return handler != null && handler.handle(span, tag, value); } - private boolean interceptUrlResourceAsNameRule(DDSpanContext span, String tag, Object value) { - if (shouldSetUrlResourceAsName) { - if (HTTP_METHOD.equals(tag)) { - final Object url = span.unsafeGetTag(HTTP_URL); - if (url != null) { - setResourceFromUrl(span, value.toString(), url); - } - } else if (HTTP_URL.equals(tag)) { - final Object method = span.unsafeGetTag(HTTP_METHOD); - setResourceFromUrl(span, method != null ? method.toString() : null, value); + // Installed only when URL_AS_RESOURCE_NAME is enabled (shouldSetUrlResourceAsName), so the gate is + // gone from the body. Returns false so the method/url tag is still stored. + private static boolean interceptUrlResourceAsNameRule(DDSpanContext span, String tag, Object value) { + if (HTTP_METHOD.equals(tag)) { + final Object url = span.unsafeGetTag(HTTP_URL); + if (url != null) { + setResourceFromUrl(span, value.toString(), url); } + } else if (HTTP_URL.equals(tag)) { + final Object method = span.unsafeGetTag(HTTP_METHOD); + setResourceFromUrl(span, method != null ? method.toString() : null, value); } return false; } @@ -356,22 +292,20 @@ private static void setResourceFromUrl( } } - private boolean interceptResourceName(DDSpanContext span, Object value) { - if (ruleFlags.isEnabled(RESOURCE_NAME)) { - if (null == value) { - return false; - } - if (value instanceof CharSequence) { - span.setResourceName((CharSequence) value, ResourceNamePriorities.TAG_INTERCEPTOR); - } else { - span.setResourceName(String.valueOf(value), ResourceNamePriorities.TAG_INTERCEPTOR); - } - return true; + // Installed only when RESOURCE_NAME is enabled, so the gate is gone from the body. + private static boolean interceptResourceName(DDSpanContext span, String tag, Object value) { + if (null == value) { + return false; } - return false; + if (value instanceof CharSequence) { + span.setResourceName((CharSequence) value, ResourceNamePriorities.TAG_INTERCEPTOR); + } else { + span.setResourceName(String.valueOf(value), ResourceNamePriorities.TAG_INTERCEPTOR); + } + return true; } - private boolean interceptDbStatement(DDSpanContext span, Object value) { + private static boolean interceptDbStatement(DDSpanContext span, String tag, Object value) { if (value instanceof CharSequence) { CharSequence resourceName = (CharSequence) value; if (resourceName.length() > 0) { @@ -381,12 +315,85 @@ private boolean interceptDbStatement(DDSpanContext span, Object value) { return true; } - private boolean interceptError(DDSpanContext span, Object value) { + // Installed only when SERVICE_NAME is enabled, so the gate is gone (the peer path below keeps the + // feature-checking overload, since it sets PEER_SERVICE_SOURCE regardless). + private static boolean interceptServiceName(DDSpanContext span, String tag, Object value) { + String serviceName = String.valueOf(value); + span.setServiceName(serviceName); + ServiceNameCollector.get().addService(serviceName); + return true; + } + + private boolean interceptPeerService(DDSpanContext span, String tag, Object value) { + // we still need to intercept and add this tag when the user manually set + span.setTag(DDTags.PEER_SERVICE_SOURCE, Tags.PEER_SERVICE); + return interceptServiceName(PEER_SERVICE, span, value); + } + + private static boolean interceptManualKeep(DDSpanContext span, String tag, Object value) { + if (asBoolean(value)) { + span.forceKeep(); + return true; + } + return false; + } + + // Installed only when FORCE_MANUAL_DROP is enabled. + private static boolean interceptManualDrop(DDSpanContext span, String tag, Object value) { + if (asBoolean(value)) { + span.setSamplingPriority(USER_DROP, SamplingMechanism.MANUAL); + } + return true; + } + + private static boolean interceptAsmKeep(DDSpanContext span, String tag, Object value) { + if (asBoolean(value)) { + span.forceKeep(SamplingMechanism.APPSEC); + return true; + } + return false; + } + + private static boolean interceptAiGuardKeep(DDSpanContext span, String tag, Object value) { + if (asBoolean(value)) { + span.forceKeep(SamplingMechanism.AI_GUARD); + return true; + } + return false; + } + + private static boolean interceptPropagatedTraceSource( + DDSpanContext span, String tag, Object value) { + if (value instanceof Integer) { + span.addPropagatedTraceSource((Integer) value); + return true; + } + return false; + } + + private static boolean interceptPropagatedDebug(DDSpanContext span, String tag, Object value) { + span.updateDebugPropagation(String.valueOf(value)); + return true; + } + + private static boolean interceptSpanKind(DDSpanContext span, String tag, Object value) { + // Cache the ordinal for fast isOutbound() checks. + // Return false so the value is still stored in unsafeTags for serialization. + span.setSpanKindOrdinal(String.valueOf(value)); + return false; + } + + private static boolean interceptSplitService(DDSpanContext span, String tag, Object value) { + span.setServiceName(String.valueOf(value), SPLIT_BY_TAGS); + return true; + } + + private static boolean interceptError(DDSpanContext span, String tag, Object value) { span.setErrorFlag(asBoolean(value), ErrorPriorities.DEFAULT); return true; } - private boolean interceptAnalyticsSampleRate(DDSpanContext span, Object value) { + private static boolean interceptAnalyticsSampleRate(DDSpanContext span, String tag, Object value) { Number analyticsSampleRate = getOrTryParse(value); if (null != analyticsSampleRate) { span.setMetric(ANALYTICS_SAMPLE_RATE, analyticsSampleRate); @@ -394,7 +401,7 @@ private boolean interceptAnalyticsSampleRate(DDSpanContext span, Object value) { return true; } - private boolean interceptSpanType(DDSpanContext span, Object value) { + private static boolean interceptSpanType(DDSpanContext span, String tag, Object value) { if (value instanceof CharSequence) { span.setSpanType((CharSequence) value); } else { @@ -413,37 +420,20 @@ boolean interceptServiceName(RuleFlags.Feature feature, DDSpanContext span, Obje return false; } - private boolean interceptSamplingPriority( - RuleFlags.Feature feature, - int samplingPriority, - int samplingMechanism, - DDSpanContext span, - Object value) { - if (ruleFlags.isEnabled(feature)) { - if (asBoolean(value)) { - span.setSamplingPriority(samplingPriority, samplingMechanism); - } - return true; - } - return false; - } - - private boolean interceptSamplingPriority(DDSpanContext span, Object value) { - if (ruleFlags.isEnabled(FORCE_SAMPLING_PRIORITY)) { - Number samplingPriority = getOrTryParse(value); - if (null != samplingPriority) { - if (samplingPriority.intValue() > 0) { - span.forceKeep(SamplingMechanism.MANUAL); - } else { - span.setSamplingPriority(USER_DROP, SamplingMechanism.MANUAL); - } + // Installed only when FORCE_SAMPLING_PRIORITY is enabled, so the gate is gone from the body. + private static boolean interceptSamplingPriority(DDSpanContext span, String tag, Object value) { + Number samplingPriority = getOrTryParse(value); + if (null != samplingPriority) { + if (samplingPriority.intValue() > 0) { + span.forceKeep(SamplingMechanism.MANUAL); + } else { + span.setSamplingPriority(USER_DROP, SamplingMechanism.MANUAL); } - return true; } - return false; + return true; } - boolean interceptServletContext(DDSpanContext span, Object value) { + boolean interceptServletContext(DDSpanContext span, String tag, Object value) { // even though this tag is sometimes used to set the service name // (which has the side effect of marking the span as eligible for metrics // in the trace agent) we also want to store it in the tags no matter what, @@ -477,7 +467,7 @@ boolean interceptServletContext(DDSpanContext span, Object value) { return false; } - private boolean interceptHttpStatusCode(DDSpanContext span, Object statusCode) { + private boolean interceptHttpStatusCode(DDSpanContext span, String tag, Object statusCode) { if (statusCode instanceof Number) { span.setHttpStatusCode(((Number) statusCode).shortValue()); if (shouldSet404ResourceName && span.getHttpStatusCode() == 404) { @@ -496,7 +486,8 @@ private boolean interceptHttpStatusCode(DDSpanContext span, Object statusCode) { return false; } - private boolean interceptOrigin(final DDSpanContext span, final Object origin) { + private static boolean interceptOrigin( + final DDSpanContext span, final String tag, final Object origin) { if (origin instanceof CharSequence) { span.setOrigin((CharSequence) origin); } else { @@ -505,7 +496,7 @@ private boolean interceptOrigin(final DDSpanContext span, final Object origin) { return true; } - private static boolean interceptMeasured(DDSpanContext span, Object value) { + private static boolean interceptMeasured(DDSpanContext span, String tag, Object value) { if ((value instanceof Number && ((Number) value).intValue() > 0) || asBoolean(value)) { span.setMeasured(true); return true; diff --git a/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java b/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java index 6f1e714334e..649b3032b3d 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/taginterceptor/TagInterceptorTest.java @@ -750,7 +750,7 @@ void whenInterceptServletContextExtraServiceProviderIsCalled(String value, Strin new TagInterceptor( true, "my-service", Collections.singleton("servlet.context"), ruleFlags, false); - interceptor.interceptServletContext(mock(DDSpanContext.class), value); + interceptor.interceptServletContext(mock(DDSpanContext.class), "servlet.context", value); verify(extraServiceProvider, times(1)).addService(expected); } finally { diff --git a/internal-api/src/main/java/datadog/trace/util/StringIndex.java b/internal-api/src/main/java/datadog/trace/util/StringIndex.java index ec16b51dd3e..3a4398cb79c 100644 --- a/internal-api/src/main/java/datadog/trace/util/StringIndex.java +++ b/internal-api/src/main/java/datadog/trace/util/StringIndex.java @@ -1,6 +1,7 @@ package datadog.trace.util; import java.lang.reflect.Array; +import java.util.Collection; import java.util.function.Function; import java.util.function.ToIntFunction; import java.util.function.ToLongFunction; @@ -59,6 +60,15 @@ public static StringIndex of(String... names) { return new StringIndex(data.hashes, data.names); } + /** + * Convenience instance over a collection of names -- e.g. a Config-driven set resolved at startup, + * where the names aren't compile-time-known so the folded static {@link Support} path isn't an + * option anyway. Empty in, empty (membership-always-false) index out. + */ + public static StringIndex of(Collection names) { + return of(names.toArray(new String[0])); + } + /** Slot of {@code name}, or -1. Delegates to {@link Support} on the instance's arrays. */ public int indexOf(String name) { return Support.indexOf(this.hashes, this.names, name);