Skip to content

Commit 20aae2c

Browse files
committed
Introduce localTraceId to disambiguate incoming requests that share traceid
Logs from concurrent incoming requests with the same traceid are currently difficult to tell apart (one can look at thread names etc, but not across executors). This introduces a new optional property on a Trace, localTraceId, that we set to a new unique identifier if an incoming request already has a traceid assigned.
1 parent 2150bd2 commit 20aae2c

File tree

6 files changed

+134
-14
lines changed

6 files changed

+134
-14
lines changed

tracing-jersey/src/main/java/com/palantir/tracing/jersey/TraceEnrichingFilter.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public final class TraceEnrichingFilter implements ContainerRequestFilter, Conta
4646
* This is the name of the trace id property we set on {@link ContainerRequestContext}.
4747
*/
4848
public static final String TRACE_ID_PROPERTY_NAME = "com.palantir.tracing.traceId";
49+
public static final String LOCAL_TRACE_ID_PROPERTY_NAME = "com.palantir.tracing.localTraceId";
4950
public static final String SAMPLED_PROPERTY_NAME = "com.palantir.tracing.sampled";
5051

5152
@Context
@@ -71,7 +72,8 @@ public void filter(ContainerRequestContext requestContext) throws IOException {
7172
Tracer.initTrace(getObservabilityFromHeader(requestContext), Tracers.randomId());
7273
Tracer.fastStartSpan(operation, SpanType.SERVER_INCOMING);
7374
} else {
74-
Tracer.initTrace(getObservabilityFromHeader(requestContext), traceId);
75+
// propagate the traceid from request headers, but create a new local trace id to disambiguate
76+
Tracer.initTrace(getObservabilityFromHeader(requestContext), traceId, Tracers.randomId());
7577
if (spanId == null) {
7678
Tracer.fastStartSpan(operation, SpanType.SERVER_INCOMING);
7779
} else {
@@ -82,6 +84,8 @@ public void filter(ContainerRequestContext requestContext) throws IOException {
8284

8385
// Give asynchronous downstream handlers access to the trace id
8486
requestContext.setProperty(TRACE_ID_PROPERTY_NAME, Tracer.getTraceId());
87+
Tracer.getLocalTraceId().ifPresent(localId ->
88+
requestContext.setProperty(LOCAL_TRACE_ID_PROPERTY_NAME, localId));
8589
requestContext.setProperty(SAMPLED_PROPERTY_NAME, Tracer.isTraceObservable());
8690
}
8791

tracing-jersey/src/test/java/com/palantir/tracing/jersey/TraceEnrichingFilterTest.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,36 @@ public void testTraceState_withHeaderUsesTraceId() {
117117
assertThat(spanCaptor.getValue().getOperation()).isEqualTo("Jersey: GET /trace");
118118
}
119119

120+
@Test
121+
public void testTraceState_withHeaderUsesTraceIdWithDifferentLocalIds() {
122+
Response response = target.path("/local-trace").request()
123+
.header(TraceHttpHeaders.TRACE_ID, "traceId")
124+
.header(TraceHttpHeaders.PARENT_SPAN_ID, "parentSpanId")
125+
.header(TraceHttpHeaders.SPAN_ID, "spanId")
126+
.get();
127+
assertThat(response.getHeaderString(TraceHttpHeaders.TRACE_ID)).isEqualTo("traceId");
128+
assertThat(response.getHeaderString(TraceHttpHeaders.PARENT_SPAN_ID)).isNull();
129+
assertThat(response.getHeaderString(TraceHttpHeaders.SPAN_ID)).isNull();
130+
verify(observer).consume(spanCaptor.capture());
131+
assertThat(spanCaptor.getValue().getOperation()).isEqualTo("Jersey: GET /local-trace");
132+
133+
String firstLocalTrace = response.readEntity(String.class);
134+
assertThat(firstLocalTrace).isNotEmpty();
135+
136+
// make the query again
137+
Response otherResponse = target.path("/local-trace").request()
138+
.header(TraceHttpHeaders.TRACE_ID, "traceId")
139+
.header(TraceHttpHeaders.PARENT_SPAN_ID, "parentSpanId")
140+
.header(TraceHttpHeaders.SPAN_ID, "spanId")
141+
.get();
142+
assertThat(otherResponse.getHeaderString(TraceHttpHeaders.TRACE_ID)).isEqualTo("traceId");
143+
assertThat(otherResponse.getHeaderString(TraceHttpHeaders.PARENT_SPAN_ID)).isNull();
144+
assertThat(otherResponse.getHeaderString(TraceHttpHeaders.SPAN_ID)).isNull();
145+
String otherLocalTrace = otherResponse.readEntity(String.class);
146+
assertThat(otherLocalTrace).isNotEmpty();
147+
assertThat(otherLocalTrace).isNotEqualTo(firstLocalTrace);
148+
}
149+
120150
@Test
121151
public void testTraceState_respectsMethod() {
122152
Response response = target.path("/trace").request()
@@ -155,6 +185,17 @@ public void testTraceState_withoutRequestHeadersGeneratesValidTraceResponseHeade
155185
assertThat(spanCaptor.getValue().getOperation()).isEqualTo("Jersey: GET /trace");
156186
}
157187

188+
@Test
189+
public void testTraceState_withoutRequestHeadersDoesNotGenerateLocalTrace() {
190+
Response response = target.path("/local-trace").request().get();
191+
assertThat(response.getHeaderString(TraceHttpHeaders.TRACE_ID)).isNotNull();
192+
assertThat(response.getHeaderString(TraceHttpHeaders.PARENT_SPAN_ID)).isNull();
193+
assertThat(response.getHeaderString(TraceHttpHeaders.SPAN_ID)).isNull();
194+
verify(observer).consume(spanCaptor.capture());
195+
assertThat(spanCaptor.getValue().getOperation()).isEqualTo("Jersey: GET /local-trace");
196+
assertThat(response.readEntity(String.class)).isNullOrEmpty();
197+
}
198+
158199
@Test
159200
public void testTraceState_withoutRequestHeadersGeneratesValidTraceResponseHeadersWhenFailing() {
160201
Response response = target.path("/failing-trace").request().get();
@@ -262,6 +303,11 @@ public void getFailingTraceOperation() {
262303
throw new RuntimeException();
263304
}
264305

306+
@Override
307+
public String getLocalTraceId() {
308+
return Tracer.getLocalTraceId().orElse(null);
309+
}
310+
265311
@Override
266312
public StreamingOutput getFailingStreamingTraceOperation() {
267313
return os -> {
@@ -296,6 +342,10 @@ public interface TracingTestService {
296342
@Path("/failing-trace")
297343
void getFailingTraceOperation();
298344

345+
@GET
346+
@Path("/local-trace")
347+
String getLocalTraceId();
348+
299349
@GET
300350
@Path("/failing-streaming-trace")
301351
@Produces(MediaType.APPLICATION_OCTET_STREAM)

tracing-undertow/src/main/java/com/palantir/tracing/undertow/TracedOperationHandler.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ private String initializeTrace(HttpServerExchange exchange) {
100100

101101
/** Initializes trace state given a trace-id header from the client. */
102102
private void initializeTraceFromExisting(HeaderMap headers, String traceId) {
103-
Tracer.initTrace(getObservabilityFromHeader(headers), traceId);
103+
// propagate the traceid from request headers, but create a new local trace id to disambiguate
104+
Tracer.initTrace(getObservabilityFromHeader(headers), traceId, Tracers.randomId());
104105
String spanId = headers.getFirst(SPAN_ID); // nullable
105106
if (spanId == null) {
106107
Tracer.fastStartSpan(operation, SpanType.SERVER_INCOMING);

tracing-undertow/src/test/java/com/palantir/tracing/undertow/TracedOperationHandlerTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,29 @@ public void whenTraceIsInHeader_usesGivenTraceId() throws Exception {
102102
assertThat(exchange.getResponseHeaders().getFirst(TraceHttpHeaders.TRACE_ID)).isEqualTo(traceId);
103103
}
104104

105+
@Test
106+
public void whenTraceIsInHeader_usesGivenTraceIdwithDifferentLocalIds() throws Exception {
107+
setRequestTraceId(traceId);
108+
AtomicReference<String> traceIdValue = new AtomicReference<>();
109+
AtomicReference<String> localTraceIdValue = new AtomicReference<>();
110+
TracedOperationHandler traceSettingHandler =
111+
new TracedOperationHandler(exc -> {
112+
traceIdValue.set(Tracer.getTraceId());
113+
localTraceIdValue.set(Tracer.getLocalTraceId().orElse(null));
114+
115+
}, "GET /traced");
116+
traceSettingHandler.handleRequest(exchange);
117+
118+
assertThat(traceIdValue.get()).isEqualTo(traceId);
119+
assertThat(localTraceIdValue).isNotNull();
120+
String firstLocalId = localTraceIdValue.get();
121+
122+
traceSettingHandler.handleRequest(exchange);
123+
assertThat(traceIdValue.get()).isEqualTo(traceId);
124+
assertThat(localTraceIdValue.get()).isNotNull();
125+
assertThat(localTraceIdValue.get()).isNotEqualTo(firstLocalId);
126+
}
127+
105128
@Test
106129
public void whenParentSpanIsGiven_usesParentSpan() throws Exception {
107130
setRequestTraceId(traceId);

tracing/src/main/java/com/palantir/tracing/Trace.java

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.util.ArrayDeque;
3030
import java.util.Deque;
3131
import java.util.Optional;
32+
import javax.annotation.Nullable;
3233

3334
/**
3435
* Represents a trace as an ordered list of non-completed spans. Supports adding and removing of spans. This class is
@@ -44,10 +45,13 @@
4445
public abstract class Trace {
4546

4647
private final String traceId;
48+
@Nullable
49+
private final String localTraceId;
4750

48-
private Trace(String traceId) {
51+
private Trace(String traceId, @Nullable String localTraceId) {
4952
checkArgument(!Strings.isNullOrEmpty(traceId), "traceId must be non-empty");
5053
this.traceId = traceId;
54+
this.localTraceId = localTraceId;
5155
}
5256

5357
/**
@@ -136,28 +140,45 @@ final String getTraceId() {
136140
return traceId;
137141
}
138142

143+
/**
144+
* The globally unique non-empty identifier for this call trace within a service (or another locality context).
145+
*
146+
* While {@link #getTraceId()} is expected to be propagated across RPC calls, localTraceId distinguishes between
147+
* two concurrent RPC calls made to a service with the same traceid.
148+
*/
149+
final Optional<String> getLocalTraceId() {
150+
// XXX: should this be equal to traceId if localTraceId is not set?
151+
return Optional.ofNullable(localTraceId);
152+
}
153+
139154
abstract Optional<String> getOriginatingSpanId();
140155

141156
/** Returns a copy of this Trace which can be independently mutated. */
142157
abstract Trace deepCopy();
143158

144159
static Trace of(boolean isObservable, String traceId) {
145-
return isObservable ? new Sampled(traceId) : new Unsampled(traceId);
160+
return isObservable ? new Sampled(traceId, null) : new Unsampled(traceId, null);
161+
}
162+
163+
static Trace of(boolean isObservable, String traceId, String localTraceId) {
164+
checkArgument(!Strings.isNullOrEmpty(localTraceId), "localTraceId must be non-empty");
165+
return isObservable ? new Sampled(traceId, localTraceId) : new Unsampled(traceId, localTraceId);
146166
}
147167

148168
private static final class Sampled extends Trace {
149169

150170
private final Deque<OpenSpan> stack;
151171

152-
private Sampled(ArrayDeque<OpenSpan> stack, String traceId) {
153-
super(traceId);
172+
private Sampled(ArrayDeque<OpenSpan> stack, String traceId, @Nullable String localTraceId) {
173+
super(traceId, localTraceId);
154174
this.stack = stack;
155175
}
156176

157-
private Sampled(String traceId) {
158-
this(new ArrayDeque<>(), traceId);
177+
private Sampled(String traceId, @Nullable String localTraceId) {
178+
this(new ArrayDeque<>(), traceId, localTraceId);
159179
}
160180

181+
161182
@Override
162183
@SuppressWarnings("ResultOfMethodCallIgnored") // Sampled traces cannot optimize this path
163184
void fastStartSpan(String operation, String parentSpanId, SpanType type) {
@@ -205,7 +226,7 @@ Optional<String> getOriginatingSpanId() {
205226

206227
@Override
207228
Trace deepCopy() {
208-
return new Sampled(new ArrayDeque<>(stack), getTraceId());
229+
return new Sampled(new ArrayDeque<>(stack), getTraceId(), getLocalTraceId().orElse(null));
209230
}
210231

211232
@Override
@@ -222,14 +243,14 @@ private static final class Unsampled extends Trace {
222243
private int numberOfSpans;
223244
private Optional<String> originatingSpanId = Optional.empty();
224245

225-
private Unsampled(int numberOfSpans, String traceId) {
226-
super(traceId);
246+
private Unsampled(int numberOfSpans, String traceId, @Nullable String localTraceId) {
247+
super(traceId, localTraceId);
227248
this.numberOfSpans = numberOfSpans;
228249
validateNumberOfSpans();
229250
}
230251

231-
private Unsampled(String traceId) {
232-
this(0, traceId);
252+
private Unsampled(String traceId, @Nullable String localTraceId) {
253+
this(0, traceId, localTraceId);
233254
}
234255

235256
@Override
@@ -289,7 +310,7 @@ Optional<String> getOriginatingSpanId() {
289310

290311
@Override
291312
Trace deepCopy() {
292-
return new Unsampled(numberOfSpans, getTraceId());
313+
return new Unsampled(numberOfSpans, getTraceId(), getLocalTraceId().orElse(null));
293314
}
294315

295316
/** Internal validation, this should never fail because {@link #pop()} only decrements positive values. */

tracing/src/main/java/com/palantir/tracing/Tracer.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ private static Trace createTrace(Observability observability, String traceId) {
7878
return Trace.of(observable, traceId);
7979
}
8080

81+
/**
82+
* Creates a new localized trace, but does not set it as the current trace.
83+
*/
84+
private static Trace createTrace(Observability observability, String traceId, String localTraceId) {
85+
checkArgument(!Strings.isNullOrEmpty(traceId), "traceId must be non-empty");
86+
boolean observable = shouldObserve(observability);
87+
return Trace.of(observable, traceId, localTraceId);
88+
}
89+
8190
private static boolean shouldObserve(Observability observability) {
8291
switch (observability) {
8392
case SAMPLE:
@@ -139,6 +148,13 @@ public static void initTrace(Observability observability, String traceId) {
139148
setTrace(createTrace(observability, traceId));
140149
}
141150

151+
/**
152+
* Initializes the current thread's trace, erasing any previously accrued open spans.
153+
*/
154+
public static void initTrace(Observability observability, String traceId, String localTraceId) {
155+
setTrace(createTrace(observability, traceId, localTraceId));
156+
}
157+
142158
/**
143159
* Opens a new span for this thread's call trace, labeled with the provided operation and parent span. Only allowed
144160
* when the current trace is empty.
@@ -483,6 +499,11 @@ public static String getTraceId() {
483499
return checkNotNull(currentTrace.get(), "There is no trace").getTraceId();
484500
}
485501

502+
/** Returns the globally unique identifier for this thread's trace specific to this call. */
503+
public static Optional<String> getLocalTraceId() {
504+
return checkNotNull(currentTrace.get(), "There is no trace").getLocalTraceId();
505+
}
506+
486507
/** Clears the current trace id and returns it if present. */
487508
static Optional<Trace> getAndClearTraceIfPresent() {
488509
Optional<Trace> trace = Optional.ofNullable(currentTrace.get());

0 commit comments

Comments
 (0)