Skip to content

Commit ccb4a4b

Browse files
dougqhclaude
andcommitted
Tighten AggregateEntry surface — drop one-line factory, doc the conventions
Five small cleanups surfaced by the design re-review: - Drop AggregateEntry.forSnapshot(SpanSnapshot, long). It wrapped the private constructor for no reason; make the constructor package- private and have AggregateTable.findOrInsert and AggregateEntryTestUtils.forSnapshot call it directly. - Class-level Javadoc now documents the required-vs-optional field absence convention: required fields canonicalize null -> EMPTY, optional fields stay null so the serializer's `!= null` check works. Previously a reader had to infer it from the constructor body. - Field Javadocs on `synthetic` (synthetic-monitoring origin tag) and `traceRoot` (parentId == 0). Both make it onto the wire; neither was obvious to a fresh reader. - Tighten the `peerTagNames` / `peerTagValues` field comment. The previous wording implied package-private was for "test-only" access; in fact production matches() reads them from within the class and the test helper is just one consumer. - Add a `canonicalizeOptional` helper that mirrors `canonicalize` but returns null (not EMPTY) for null input. Folds the four optional- field assignments in the constructor from three-line ternaries into one-liners. Keeps the `instanceof UTF8BytesString` short-circuit consistent across all label fields -- dead code for the String-typed optionals (httpMethod/Endpoint/grpcStatusCode), live for the CharSequence-typed serviceNameSource. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent acf2ffa commit ccb4a4b

4 files changed

Lines changed: 49 additions & 36 deletions

File tree

dd-trace-core/src/main/java/datadog/trace/common/metrics/AggregateEntry.java

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@
3737
* key. The class is wider than its predecessors as a result, but that's the trade we explicitly
3838
* chose.
3939
*
40+
* <p><b>Required vs optional field absence.</b> Required label fields ({@code resource}, {@code
41+
* service}, {@code operationName}, {@code type}, {@code spanKind}) canonicalize a {@code null}
42+
* snapshot value into {@link UTF8BytesString#EMPTY} via {@link #canonicalize} -- they are never
43+
* {@code null} on a constructed entry. Optional label fields ({@code serviceSource}, {@code
44+
* httpMethod}, {@code httpEndpoint}, {@code grpcStatusCode}) stay {@code null} on the entry when
45+
* the snapshot value was {@code null}; the serializer uses {@code != null} to decide whether to
46+
* emit them on the wire. {@link #contentEquals} treats {@code null} and length-0 as equivalent so
47+
* {@link #matches} works against either form.
48+
*
4049
* <p><b>Not thread-safe.</b> Counter and histogram updates are performed by the single aggregator
4150
* thread; producer threads tag durations via {@link #ERROR_TAG} / {@link #TOP_LEVEL_TAG} bits and
4251
* hand them off through the snapshot inbox.
@@ -98,16 +107,20 @@ final class AggregateEntry extends Hashtable.Entry {
98107
@Nullable private final UTF8BytesString httpEndpoint;
99108
@Nullable private final UTF8BytesString grpcStatusCode;
100109
private final short httpStatusCode;
110+
111+
/** Whether the root span carried the {@code synthetics} origin tag (synthetic-monitoring run). */
101112
private final boolean synthetic;
113+
114+
/** Whether this span is the trace root ({@code parentId == 0}). */
102115
private final boolean traceRoot;
103116

104117
// Peer tags carried in two forms: parallel String[] arrays mirroring the snapshot's (schema +
105118
// values) shape for matches(), and pre-encoded List<UTF8BytesString> ("name:value") for the
106119
// serializer. peerTagNames is the schema's names array (shared by-reference when the schema
107120
// hasn't been replaced); peerTagValues is the per-span String[] parallel to it.
108121
//
109-
// Package-private rather than private so test-only helpers (e.g. argument-matcher classes in
110-
// the same package) can compare them without going through the encoded list.
122+
// Package-private so the in-package test helper (AggregateEntryTestUtils) can compare entries
123+
// by raw layout; production access comes from this class's own matches() + constructor.
111124
@Nullable final String[] peerTagNames;
112125
@Nullable final String[] peerTagValues;
113126
private final List<UTF8BytesString> peerTags;
@@ -121,29 +134,17 @@ final class AggregateEntry extends Hashtable.Entry {
121134
private long duration;
122135

123136
/** Hot-path constructor for the producer/consumer flow. Builds UTF8 fields via the caches. */
124-
private AggregateEntry(SpanSnapshot s, long keyHash) {
137+
AggregateEntry(SpanSnapshot s, long keyHash) {
125138
super(keyHash);
126139
this.resource = canonicalize(RESOURCE_CACHE, s.resourceName);
127140
this.service = canonicalize(SERVICE_CACHE, s.serviceName);
128141
this.operationName = canonicalize(OPERATION_CACHE, s.operationName);
129-
this.serviceSource =
130-
s.serviceNameSource == null
131-
? null
132-
: canonicalize(SERVICE_SOURCE_CACHE, s.serviceNameSource);
142+
this.serviceSource = canonicalizeOptional(SERVICE_SOURCE_CACHE, s.serviceNameSource);
133143
this.type = canonicalize(TYPE_CACHE, s.spanType);
134144
this.spanKind = canonicalize(SPAN_KIND_CACHE, s.spanKind);
135-
this.httpMethod =
136-
s.httpMethod == null
137-
? null
138-
: HTTP_METHOD_CACHE.computeIfAbsent(s.httpMethod, UTF8BytesString::create);
139-
this.httpEndpoint =
140-
s.httpEndpoint == null
141-
? null
142-
: HTTP_ENDPOINT_CACHE.computeIfAbsent(s.httpEndpoint, UTF8BytesString::create);
143-
this.grpcStatusCode =
144-
s.grpcStatusCode == null
145-
? null
146-
: GRPC_STATUS_CODE_CACHE.computeIfAbsent(s.grpcStatusCode, UTF8BytesString::create);
145+
this.httpMethod = canonicalizeOptional(HTTP_METHOD_CACHE, s.httpMethod);
146+
this.httpEndpoint = canonicalizeOptional(HTTP_ENDPOINT_CACHE, s.httpEndpoint);
147+
this.grpcStatusCode = canonicalizeOptional(GRPC_STATUS_CODE_CACHE, s.grpcStatusCode);
147148
this.httpStatusCode = s.httpStatusCode;
148149
this.synthetic = s.synthetic;
149150
this.traceRoot = s.traceRoot;
@@ -152,15 +153,6 @@ private AggregateEntry(SpanSnapshot s, long keyHash) {
152153
this.peerTags = materializePeerTags(this.peerTagNames, this.peerTagValues);
153154
}
154155

155-
/**
156-
* Construct from a snapshot at consumer-thread miss time, using the {@code keyHash} the caller
157-
* (typically {@link AggregateTable#findOrInsert}) already computed for the lookup. Avoids a
158-
* second pass over the snapshot's fields just to re-hash them.
159-
*/
160-
static AggregateEntry forSnapshot(SpanSnapshot s, long keyHash) {
161-
return new AggregateEntry(s, keyHash);
162-
}
163-
164156
/**
165157
* Records a single hit. {@code tagAndDuration} carries the duration nanos with optional {@link
166158
* #ERROR_TAG} / {@link #TOP_LEVEL_TAG} bits OR-ed in.
@@ -358,6 +350,28 @@ private static UTF8BytesString canonicalize(
358350
return cache.computeIfAbsent(charSeq.toString(), UTF8BytesString::create);
359351
}
360352

353+
/**
354+
* Like {@link #canonicalize} but returns {@code null} for a {@code null} input (rather than
355+
* {@link UTF8BytesString#EMPTY}). Used for the four optional fields so the serializer can
356+
* distinguish "absent" via a {@code != null} check and elide the field on the wire.
357+
*
358+
* <p>The {@code instanceof UTF8BytesString} short-circuit is dead code for {@link
359+
* SpanSnapshot#httpMethod}/{@code httpEndpoint}/{@code grpcStatusCode} (statically {@code
360+
* String}) but live for {@link SpanSnapshot#serviceNameSource} ({@link CharSequence}); keeping a
361+
* single helper keeps the constructor consistent.
362+
*/
363+
@Nullable
364+
private static UTF8BytesString canonicalizeOptional(
365+
DDCache<String, UTF8BytesString> cache, @Nullable CharSequence charSeq) {
366+
if (charSeq == null) {
367+
return null;
368+
}
369+
if (charSeq instanceof UTF8BytesString) {
370+
return (UTF8BytesString) charSeq;
371+
}
372+
return cache.computeIfAbsent(charSeq.toString(), UTF8BytesString::create);
373+
}
374+
361375
/**
362376
* UTF8 vs raw CharSequence content-equality, no allocation in the common (String) case.
363377
*

dd-trace-core/src/main/java/datadog/trace/common/metrics/AggregateTable.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ AggregateEntry findOrInsert(SpanSnapshot snapshot) {
6666
if (size >= maxAggregates && !evictOneStale()) {
6767
return null;
6868
}
69-
AggregateEntry entry = AggregateEntry.forSnapshot(snapshot, keyHash);
69+
AggregateEntry entry = new AggregateEntry(snapshot, keyHash);
7070
Support.insertHeadEntry(buckets, keyHash, entry);
7171
size++;
7272
return entry;

dd-trace-core/src/test/java/datadog/trace/common/metrics/AggregateEntryTestUtils.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,12 @@ public static AggregateEntry of(
9090

9191
/**
9292
* Builds an {@link AggregateEntry} from {@code s} by computing its lookup hash via {@link
93-
* AggregateEntry#hashOf(SpanSnapshot)} and delegating to {@link
94-
* AggregateEntry#forSnapshot(SpanSnapshot, long)}. Production callers route through {@link
95-
* AggregateTable#findOrInsert} which already has the {@code keyHash} on hand; tests rarely do, so
96-
* this helper hides the second argument.
93+
* AggregateEntry#hashOf(SpanSnapshot)} and calling the package-private constructor directly.
94+
* Production callers route through {@link AggregateTable#findOrInsert} which already has the
95+
* {@code keyHash} on hand; tests rarely do, so this helper hides the second argument.
9796
*/
9897
public static AggregateEntry forSnapshot(SpanSnapshot s) {
99-
return AggregateEntry.forSnapshot(s, AggregateEntry.hashOf(s));
98+
return new AggregateEntry(s, AggregateEntry.hashOf(s));
10099
}
101100

102101
/**

dd-trace-core/src/traceAgentTest/groovy/MetricsIntegrationTest.groovy

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,13 @@ class MetricsIntegrationTest extends AbstractTraceAgentTest {
4646
SpanSnapshot snap1 = new SpanSnapshot(
4747
"resource1", "service1", "operation1", null, "sql", (short) 0,
4848
false, true, "xyzzy", schema, ["quux"] as String[], null, null, null, 0L)
49-
def entry1 = AggregateEntry.forSnapshot(snap1, AggregateEntry.hashOf(snap1))
49+
def entry1 = new AggregateEntry(snap1, AggregateEntry.hashOf(snap1))
5050
[2, 1, 2, 250, 4].each { entry1.recordOneDuration(it as long) }
5151
writer.add(entry1)
5252
SpanSnapshot snap2 = new SpanSnapshot(
5353
"resource2", "service2", "operation2", null, "web", (short) 200,
5454
false, true, "xyzzy", schema, ["quux"] as String[], null, null, null, 0L)
55-
def entry2 = AggregateEntry.forSnapshot(snap2, AggregateEntry.hashOf(snap2))
55+
def entry2 = new AggregateEntry(snap2, AggregateEntry.hashOf(snap2))
5656
[1, 1, 200, 2, 3, 4, 5, 6, 7, 8].each { entry2.recordOneDuration(it as long) }
5757
writer.add(entry2)
5858
writer.finishBucket()

0 commit comments

Comments
 (0)