-
Notifications
You must be signed in to change notification settings - Fork 344
Add span-derived primary tags (CSS v1.3.0) #11402
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
5a52622
ece78c9
dbb1702
545e74c
9983a59
d2e4477
24969db
b6c4f5f
14f7f58
b953b3a
40e8cbd
d88a863
d01036f
7b6c5f1
10ca111
4610078
713aa34
617bc51
e1dec83
2336bb5
5b6c5aa
3c3c8b1
66ec7f6
be823ab
10956b2
035dc09
7728b60
8cd2d86
c689ef9
75790eb
6056ff7
da55021
8b8b088
0fde7cd
6d6c2e0
268de2b
93813b9
11a58bf
8f1828d
c0d3e26
a0978ba
e604a8f
e2642cd
585ca56
aab5b3b
88f169f
d61a3c6
795ba76
e455801
c2ff012
1df8bd2
e766fd3
8cfa4a5
3644470
1d1ba4d
57cb018
e7d0b42
3d4f380
9391c48
ba3225c
517ef75
9bbe2d0
ec857b2
ba0d390
0b86066
f71384e
5c78dbb
471f8f7
70c20ef
7a074e0
68848ad
3fa7ac7
df3b31d
2ea61c5
8ec956d
a606692
913e7d7
2dcea9a
50b06e5
b0f21bf
5a4685f
200028d
0c50037
078382f
7455fcc
4171d15
a1863db
3fb86d3
e5cfb54
a75bc4a
4871892
5f73c2d
9dddf0a
2c8ad26
80778c4
21e7545
877d95c
e2f2585
2536aa2
c0449a3
be13431
d174938
641b5ef
7eedb25
ce5b38b
2eaa58f
cd56981
d1df95e
4a4b72e
b382df5
e4ed029
133d3c1
2ae738b
f68ad4f
620241e
6fdc2de
a6b7854
3f32dfd
3d8cccc
2d1536c
ad01d7d
5bf4d90
60edaa4
6b10345
a67ac23
2c3730e
1eb6826
49124e5
d5065f2
db08d58
e6f2399
553bfbd
5e72808
703c9e1
92a9af3
3f4c1c6
6dc6701
dcb1898
a06c2a8
acf2ffa
ccb4a4b
1469811
d0c0be8
4af16c1
c48399d
dcd3407
f2ee559
690b796
0c658dd
e61f9cd
8c92dc0
410790c
2170c16
d99e902
07bb401
4bdfa62
60a33f7
e13c6aa
f3b4766
bebb2e1
ecc7ab6
35ecd21
23ddc21
70a7112
8fa0c9d
9d9c40e
a77edbd
d5dc2f8
3dea398
43cfb41
28646de
4c1afcf
68c05c3
e4b1d19
0c4b7ef
0d37095
0a084fd
823d107
408e04c
57bd568
610ace5
da11276
cf30c28
ec44b14
680c34d
4c4186d
c75fc16
6214ca7
79ed7f1
1489ce7
20e625b
1689e54
ca5e5ef
e53f44d
343dd3e
add5451
653fe6f
17e96dd
2cb1f0d
3c6ca3e
230dc4a
9fd6a17
05a5ef7
654305b
e991d03
7f8ff30
18315db
51702c3
6a363f3
934e599
026e82b
93e145c
4c32f87
1b5304a
80dbbab
432a713
62c68e6
69b64c8
f1aad6c
aa1587e
dbfd69e
248d7f9
6d18a35
f647cff
b08d970
b894b4f
2c41389
e6e7d31
d92f060
2aab39e
c04a107
df657af
ff1f53c
08c9f9e
c4f56b5
9e13d22
3b3ff93
f990b9c
ef97e00
e222c3b
478e71f
e641665
153401c
89c7d6c
b56dc53
47a4cce
e29d972
f537690
1162f34
504e291
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| package datadog.trace.common.metrics; | ||
|
|
||
| import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND; | ||
| import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_CLIENT; | ||
| import static java.util.concurrent.TimeUnit.SECONDS; | ||
|
|
||
| import datadog.trace.api.WellKnownTags; | ||
| import datadog.trace.core.CoreSpan; | ||
| import de.thetaphi.forbiddenapis.SuppressForbidden; | ||
| import java.util.Arrays; | ||
| import java.util.Collections; | ||
| import java.util.LinkedHashSet; | ||
| import java.util.List; | ||
| import java.util.concurrent.ThreadLocalRandom; | ||
| import org.openjdk.jmh.annotations.Benchmark; | ||
| import org.openjdk.jmh.annotations.BenchmarkMode; | ||
| import org.openjdk.jmh.annotations.Fork; | ||
| import org.openjdk.jmh.annotations.Measurement; | ||
| import org.openjdk.jmh.annotations.Mode; | ||
| import org.openjdk.jmh.annotations.OutputTimeUnit; | ||
| import org.openjdk.jmh.annotations.Param; | ||
| import org.openjdk.jmh.annotations.Scope; | ||
| import org.openjdk.jmh.annotations.Setup; | ||
| import org.openjdk.jmh.annotations.State; | ||
| import org.openjdk.jmh.annotations.TearDown; | ||
| import org.openjdk.jmh.annotations.Threads; | ||
| import org.openjdk.jmh.annotations.Warmup; | ||
| import org.openjdk.jmh.infra.Blackhole; | ||
|
|
||
| /** | ||
| * Regression benchmark for the additional-tags hot path; {@code limitsEnabled=true} is slower here | ||
| * because it records more (sentinel collapses), not because limiting is expensive. | ||
| */ | ||
| @State(Scope.Benchmark) | ||
| @Warmup(iterations = 2, time = 15, timeUnit = SECONDS) | ||
| @Measurement(iterations = 5, time = 15, timeUnit = SECONDS) | ||
| @BenchmarkMode(Mode.Throughput) | ||
| @OutputTimeUnit(SECONDS) | ||
| @Threads(8) | ||
| @Fork(value = 1) | ||
| public class AdditionalTagsMetricsBenchmark { | ||
|
|
||
| private ClientStatsAggregator aggregator; | ||
| private AdversarialMetricsBenchmark.CountingHealthMetrics health; | ||
|
|
||
| @Param({"false", "true"}) | ||
| public boolean limitsEnabled; | ||
|
|
||
| @State(Scope.Thread) | ||
| public static class ThreadState { | ||
| int cursor; | ||
| } | ||
|
|
||
| @Setup | ||
| public void setup() { | ||
| this.health = new AdversarialMetricsBenchmark.CountingHealthMetrics(); | ||
| AdditionalTagsSchema additionalTagsSchema = | ||
| AdditionalTagsSchema.from( | ||
| new LinkedHashSet<>(Arrays.asList("region", "tenant_id")), | ||
| MetricCardinalityLimits.ADDITIONAL_TAG_VALUE, | ||
| limitsEnabled, | ||
| this.health); | ||
| this.aggregator = | ||
| new ClientStatsAggregator( | ||
| new WellKnownTags("", "", "", "", "", ""), | ||
| Collections.emptySet(), | ||
| additionalTagsSchema, | ||
| new ClientStatsAggregatorBenchmark.FixedAgentFeaturesDiscovery( | ||
| Collections.singleton("peer.hostname"), Collections.emptySet()), | ||
| this.health, | ||
| new ClientStatsAggregatorBenchmark.NullSink(), | ||
| 2048, | ||
| 2048, | ||
| false); | ||
| this.aggregator.start(); | ||
| } | ||
|
|
||
| @TearDown | ||
| @SuppressForbidden | ||
| public void tearDown() { | ||
| aggregator.close(); | ||
| System.err.println("[ADDITIONAL-TAGS] counters (across all threads, single fork):"); | ||
| System.err.println(" onStatsInboxFull = " + health.inboxFull.sum()); | ||
| System.err.println(" onStatsAggregateDropped = " + health.aggregateDropped.sum()); | ||
| } | ||
|
|
||
| @Benchmark | ||
| public void publish(ThreadState ts, Blackhole blackhole) { | ||
| int idx = ts.cursor++; | ||
| ThreadLocalRandom rng = ThreadLocalRandom.current(); | ||
|
|
||
| int scrambled = idx * 0x9E3779B1; | ||
| String region = "region-" + ((scrambled >>> 4) & 0xFFFF); | ||
| String tenant = "tenant-" + ((scrambled >>> 16) & 0xFFFF); | ||
| long durationNanos = 1L + (rng.nextLong() & 0x3FFFFFFFL); | ||
|
|
||
| SimpleSpan span = | ||
| new SimpleSpan("svc", "op", "res", "web", true, true, false, 0, durationNanos, 200); | ||
| span.setTag(SPAN_KIND, SPAN_KIND_CLIENT); | ||
| span.setTag("region", region); | ||
| span.setTag("tenant_id", tenant); | ||
|
|
||
| List<CoreSpan<?>> trace = Collections.singletonList(span); | ||
| blackhole.consume(aggregator.publish(trace)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| package datadog.trace.common.metrics; | ||
|
|
||
| import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; | ||
| import datadog.trace.core.monitor.HealthMetrics; | ||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import java.util.Set; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| /** | ||
| * Immutable schema of configured span-derived primary tag keys; built once at aggregator | ||
| * construction. | ||
| */ | ||
| final class AdditionalTagsSchema { | ||
|
|
||
| private static final Logger log = LoggerFactory.getLogger(AdditionalTagsSchema.class); | ||
|
|
||
| // Backend stats pipeline only supports a few primary tag dimensions; drop overflow at startup. | ||
| static final int MAX_ADDITIONAL_TAG_KEYS = 4; | ||
|
|
||
| /** Singleton empty schema returned when no additional tags are configured. */ | ||
| static final AdditionalTagsSchema EMPTY = | ||
| new AdditionalTagsSchema(new String[0], new TagCardinalityHandler[0], HealthMetrics.NO_OP); | ||
|
|
||
| final String[] names; | ||
|
|
||
| /** Per-key handlers providing UTF8 caching and per-cycle cardinality limiting. */ | ||
| private final TagCardinalityHandler[] handlers; | ||
|
|
||
| private final HealthMetrics healthMetrics; | ||
|
|
||
| private AdditionalTagsSchema( | ||
| String[] names, TagCardinalityHandler[] handlers, HealthMetrics healthMetrics) { | ||
| this.names = names; | ||
| this.handlers = handlers; | ||
| this.healthMetrics = healthMetrics; | ||
| } | ||
|
|
||
| /** Test convenience: uses {@link HealthMetrics#NO_OP} and limits enabled. */ | ||
| static AdditionalTagsSchema from(Set<String> configured) { | ||
| return from( | ||
| configured, MetricCardinalityLimits.ADDITIONAL_TAG_VALUE, true, HealthMetrics.NO_OP); | ||
| } | ||
|
|
||
| static AdditionalTagsSchema from( | ||
| Set<String> configured, int limit, boolean useBlockedSentinel, HealthMetrics healthMetrics) { | ||
| if (configured == null || configured.isEmpty()) { | ||
| return EMPTY; | ||
| } | ||
| List<String> valid = new ArrayList<>(); | ||
| for (String key : configured) { | ||
| if (key == null || key.isEmpty()) { | ||
| log.warn("Ignoring empty additional metric tag key"); | ||
| continue; | ||
| } | ||
| if (key.contains(":")) { | ||
| log.warn("Ignoring additional metric tag key '{}': keys must not contain ':'", key); | ||
| continue; | ||
| } | ||
| valid.add(key); | ||
| } | ||
| if (valid.isEmpty()) { | ||
| return EMPTY; | ||
| } | ||
| Collections.sort(valid); | ||
| // Dedup (sort brings duplicates adjacent) | ||
| List<String> deduped = new ArrayList<>(valid.size()); | ||
| String prev = null; | ||
| for (String key : valid) { | ||
| if (!key.equals(prev)) { | ||
| deduped.add(key); | ||
| prev = key; | ||
| } | ||
| } | ||
| if (deduped.size() > MAX_ADDITIONAL_TAG_KEYS) { | ||
| log.warn( | ||
| "Configured additional metric tag keys ({}) exceeds the supported limit of {}; " | ||
| + "dropping extra keys: {}", | ||
| deduped.size(), | ||
| MAX_ADDITIONAL_TAG_KEYS, | ||
| deduped.subList(MAX_ADDITIONAL_TAG_KEYS, deduped.size())); | ||
| deduped = deduped.subList(0, MAX_ADDITIONAL_TAG_KEYS); | ||
| } | ||
| String[] namesArr = deduped.toArray(new String[0]); | ||
| TagCardinalityHandler[] handlersArr = new TagCardinalityHandler[namesArr.length]; | ||
| for (int i = 0; i < namesArr.length; i++) { | ||
| handlersArr[i] = | ||
| new TagCardinalityHandler( | ||
| namesArr[i], | ||
| limit, | ||
| useBlockedSentinel, | ||
| MetricCardinalityLimits.ADDITIONAL_TAG_MAX_VALUE_LENGTH); | ||
| } | ||
| return new AdditionalTagsSchema(namesArr, handlersArr, healthMetrics); | ||
| } | ||
|
|
||
| int size() { | ||
| return names.length; | ||
| } | ||
|
|
||
| String name(int i) { | ||
| return names[i]; | ||
| } | ||
|
|
||
| UTF8BytesString register(int i, String value) { | ||
| return handlers[i].register(value); | ||
| } | ||
|
|
||
| void resetHandlers() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this schema class designed to have a It's primarily a design question - not recommending a better design here. I want to understand the rationale behind it or whether I'm missing an important design piece 🙇 . |
||
| for (int i = 0; i < handlers.length; i++) { | ||
| long blocked = handlers[i].reset(); | ||
| if (blocked > 0) { | ||
| log.warn( | ||
| "Cardinality limit reached for additional metric tag '{}'; further values will be reported as tracer_blocked_value", | ||
| names[i]); | ||
| healthMetrics.onTagCardinalityBlocked(handlers[i].statsDTag(), blocked); | ||
| } | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious, why do we choose to run benchmarks via annotations instead of programmatically via
OptionsBuilder?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly, that's just what we've done historically.
Although in other places, I'm pushing for minimum number of threads & forks, so allocation translates into an obvious throughput hit. That was missed in a lot of places previously.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense to me - is this work we have recorded / planned somewhere? If not, we can create a ticket and expand on it later
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My question came from a re-usability standpoint in case we plan to pass configuration there unless there is a way to read config with annotations