Skip to content

Commit 28e6f45

Browse files
authored
Fix parent/child issues with inferred spans (#1117)
1 parent a0fd551 commit 28e6f45

File tree

23 files changed

+2284
-87
lines changed

23 files changed

+2284
-87
lines changed

CHANGELOG.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ This is not a breaking change, so former
3939
When set together with one of the more specific thresholds - `trace_methods_duration_threshold` or `profiling_inferred_spans_min_duration`,
4040
the higher threshold will determine which spans will be discarded.
4141
* Automatically instrument quartz jobs from the quartz-jobs artifact {pull}1170[#1170]
42+
* Perform re-parenting of regular spans to be a child of profiler-inferred spans. Requires APM Server and Kibana 7.8.0. {pull}1117[#1117]
4243
4344
[float]
4445
===== Bug fixes

apm-agent-benchmarks/src/main/java/co/elastic/apm/agent/benchmark/ProfilerBenchmark.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ public void setUp() throws Exception {
6262
new SystemNanoClock(),
6363
new File(getClass().getClassLoader().getResource("apm-activation-events.bin").toURI()),
6464
new File(getClass().getClassLoader().getResource("apm-traces.jfr").toURI()));
65-
samplingProfiler.skipToEndOfActivationEventsFile();
6665
}
6766

6867
@TearDown
@@ -72,6 +71,7 @@ public void tearDownProfilerBenchmark() throws Exception {
7271

7372
@Benchmark
7473
public void processTraces() throws IOException {
74+
samplingProfiler.skipToEndOfActivationEventsFile();
7575
samplingProfiler.processTraces();
7676
samplingProfiler.clearProfiledThreads();
7777
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*-
2+
* #%L
3+
* Elastic APM Java agent
4+
* %%
5+
* Copyright (C) 2018 - 2020 Elastic and contributors
6+
* %%
7+
* Licensed to Elasticsearch B.V. under one or more contributor
8+
* license agreements. See the NOTICE file distributed with
9+
* this work for additional information regarding copyright
10+
* ownership. Elasticsearch B.V. licenses this file to you under
11+
* the Apache License, Version 2.0 (the "License"); you may
12+
* not use this file except in compliance with the License.
13+
* You may obtain a copy of the License at
14+
*
15+
* http://www.apache.org/licenses/LICENSE-2.0
16+
*
17+
* Unless required by applicable law or agreed to in writing,
18+
* software distributed under the License is distributed on an
19+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
20+
* KIND, either express or implied. See the License for the
21+
* specific language governing permissions and limitations
22+
* under the License.
23+
* #L%
24+
*/
25+
package co.elastic.apm.agent.collections;
26+
27+
import java.util.Arrays;
28+
29+
public class LongList {
30+
private static final int DEFAULT_CAPACITY = 16;
31+
private long[] longs;
32+
private int size;
33+
34+
public LongList() {
35+
this(DEFAULT_CAPACITY);
36+
}
37+
38+
public LongList(int initialCapacity) {
39+
longs = new long[initialCapacity];
40+
}
41+
42+
public static LongList of(long... values) {
43+
LongList list = new LongList(values.length);
44+
for (long value : values) {
45+
list.add(value);
46+
}
47+
return list;
48+
}
49+
50+
public void add(long l) {
51+
ensureCapacity(size + 1);
52+
longs[size++] = l;
53+
}
54+
55+
public void addAll(LongList other) {
56+
ensureCapacity(size + other.size);
57+
System.arraycopy(other.longs, 0, longs, size, other.size);
58+
size += other.size;
59+
}
60+
61+
private void ensureCapacity(int size) {
62+
if (longs.length < size) {
63+
longs = Arrays.copyOf(longs, longs.length * 2);
64+
}
65+
}
66+
67+
public int getSize() {
68+
return size;
69+
}
70+
71+
public long get(int i) {
72+
if (i >= size) {
73+
throw new IndexOutOfBoundsException();
74+
}
75+
return longs[i];
76+
}
77+
78+
public boolean contains(long l) {
79+
for (int i = 0; i < size; i++) {
80+
if (longs[i] == l) {
81+
return true;
82+
}
83+
}
84+
return false;
85+
}
86+
87+
public boolean remove(long l) {
88+
for (int i = size - 1; i >= 0; i--) {
89+
if (longs[i] == l) {
90+
remove(i);
91+
return true;
92+
}
93+
}
94+
return false;
95+
}
96+
97+
public long remove(int i) {
98+
long previousValue = get(i);
99+
size--;
100+
if (size > i) {
101+
System.arraycopy(longs, i + 1 , longs, i, size - i);
102+
}
103+
longs[size] = 0;
104+
return previousValue;
105+
}
106+
107+
public void clear() {
108+
Arrays.fill(longs, 0);
109+
size = 0;
110+
}
111+
112+
@Override
113+
public String toString() {
114+
StringBuilder sb = new StringBuilder();
115+
sb.append('[');
116+
for (int i = 0; i < size; i++) {
117+
if (i > 0) {
118+
sb.append(',');
119+
}
120+
sb.append(longs[i]);
121+
}
122+
sb.append(']');
123+
return sb.toString();
124+
}
125+
126+
public long[] toArray() {
127+
return Arrays.copyOfRange(longs, 0, size);
128+
}
129+
}

apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/AbstractSpan.java

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
*/
2525
package co.elastic.apm.agent.impl.transaction;
2626

27+
import co.elastic.apm.agent.collections.LongList;
2728
import co.elastic.apm.agent.configuration.CoreConfiguration;
2829
import co.elastic.apm.agent.impl.ElasticApmTracer;
2930
import co.elastic.apm.agent.impl.Scope;
@@ -69,6 +70,32 @@ public abstract class AbstractSpan<T extends AbstractSpan<T>> implements Recycla
6970
* Flag to mark a span as representing an exit event
7071
*/
7172
private boolean isExit;
73+
/**
74+
* <p>
75+
* This use case for child ids is modifying parent/child relationships for profiler-inferred spans.
76+
* Inferred spans are sent after a profiling session ends (5s by default) and after stack traces have been processed into inferred spans.
77+
* Regular spans are sent right after the event (for example a DB call) has occurred.
78+
* The effect is that a regular span cannot have a {@link TraceContext#parentId} pointing to an inferred span.
79+
* That is because the latter did not exist at the time the regular span has been created.
80+
* </p>
81+
* <p>
82+
* To work around this problem, inferred spans can point to their children.
83+
* The UI does an operation known as "transitive reduction".
84+
* What this does in this scenario is that it ignores the parent ID of a regular span if there's an inferred span
85+
* with a {@code child_id} for this span.
86+
* </p>
87+
* <pre>
88+
* ██████████████████████████████ transaction
89+
* ↑ ↑ parent_id
90+
* ╷ └──████████████████████ inferred span
91+
* ╷ ↓ child_id
92+
* └╶╶╶╶╶╶╶╶╶██████████ DB span
93+
* parent_id
94+
* (removed via transitive reduction)
95+
* </pre>
96+
*/
97+
@Nullable
98+
private LongList childIds;
7299

73100
public int getReferenceCount() {
74101
return references.get();
@@ -254,7 +281,7 @@ public T appendToName(CharSequence cs, int priority) {
254281
this.name.append(cs);
255282
this.namePriority = priority;
256283
}
257-
return (T) this;
284+
return thiz();
258285
}
259286

260287
public T withName(@Nullable String name) {
@@ -272,7 +299,7 @@ public T withName(@Nullable String name, int priority, boolean overrideIfSamePri
272299
this.name.append(name);
273300
this.namePriority = priority;
274301
}
275-
return (T) this;
302+
return thiz();
276303
}
277304

278305
/**
@@ -298,10 +325,7 @@ public void resetState() {
298325
namePriority = PRIO_DEFAULT;
299326
discardRequested = false;
300327
isExit = false;
301-
}
302-
303-
public boolean isChildOf(AbstractSpan<?> parent) {
304-
return traceContext.isChildOf(parent.traceContext);
328+
childIds = null;
305329
}
306330

307331
public Span createSpan() {
@@ -407,6 +431,17 @@ public final void end(long epochMicros) {
407431

408432
protected abstract void afterEnd();
409433

434+
public boolean isChildOf(AbstractSpan<?> parent) {
435+
return traceContext.isChildOf(parent.traceContext) || parent.hasChildId(traceContext.getId());
436+
}
437+
438+
private boolean hasChildId(Id spanId) {
439+
if (childIds != null) {
440+
return childIds.contains(spanId.readLong(0));
441+
}
442+
return false;
443+
}
444+
410445
public T activate() {
411446
tracer.activate(this);
412447
return (T) this;
@@ -559,4 +594,15 @@ public boolean isSampled() {
559594
return getTraceContext().isSampled();
560595
}
561596

597+
public T withChildIds(@Nullable LongList childIds) {
598+
this.childIds = childIds;
599+
return thiz();
600+
}
601+
602+
@Nullable
603+
public LongList getChildIds() {
604+
return childIds;
605+
}
606+
607+
protected abstract T thiz();
562608
}

apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/Id.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,7 @@ public void fromLongs(long... values) {
101101

102102
@Override
103103
public void resetState() {
104-
for (int i = 0; i < data.length; i++) {
105-
data[i] = 0;
106-
}
104+
Arrays.fill(data, (byte) 0);
107105
onMutation(true);
108106
}
109107

apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/Span.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,11 @@ protected void recycle() {
296296
tracer.recycle(this);
297297
}
298298

299+
@Override
300+
protected Span thiz() {
301+
return this;
302+
}
303+
299304
public void setStackTrace(List<StackFrame> stackTrace) {
300305
this.stackFrames = stackTrace;
301306
}

apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/TraceContext.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@
7575
* 2, 1]
7676
* </pre>
7777
*/
78-
@SuppressWarnings({"rawtypes"})
7978
public class TraceContext implements Recyclable {
8079

8180
public static final String ELASTIC_TRACE_PARENT_TEXTUAL_HEADER_NAME = "elastic-apm-traceparent";
@@ -675,6 +674,12 @@ public boolean equals(Object o) {
675674
traceId.equals(that.traceId);
676675
}
677676

677+
public boolean idEquals(@Nullable TraceContext o) {
678+
if (this == o) return true;
679+
if (o == null) return false;
680+
return id.equals(o.id);
681+
}
682+
678683
@Override
679684
public int hashCode() {
680685
return Objects.hash(traceId, id, parentId, flags);
@@ -745,6 +750,14 @@ public void deserialize(byte[] buffer, @Nullable String serviceName) {
745750
onMutation();
746751
}
747752

753+
public static void deserializeSpanId(Id id, byte[] buffer) {
754+
id.fromBytes(buffer, 16);
755+
}
756+
757+
public static long getSpanId(byte[] serializedTraceContext) {
758+
return ByteUtils.getLong(serializedTraceContext, 16);
759+
}
760+
748761
public boolean traceIdAndIdEquals(byte[] serialized) {
749762
return id.dataEquals(serialized, traceId.getLength()) && traceId.dataEquals(serialized, 0);
750763
}

apm-agent-core/src/main/java/co/elastic/apm/agent/impl/transaction/Transaction.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,11 @@ protected void recycle() {
298298
tracer.recycle(this);
299299
}
300300

301+
@Override
302+
protected Transaction thiz() {
303+
return this;
304+
}
305+
301306
void incrementTimer(@Nullable String type, @Nullable String subtype, long duration) {
302307
long criticalValueAtEnter = phaser.writerCriticalSectionEnter();
303308
try {

apm-agent-core/src/main/java/co/elastic/apm/agent/report/serialize/DslJsonSerializer.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
*/
2525
package co.elastic.apm.agent.report.serialize;
2626

27+
import co.elastic.apm.agent.collections.LongList;
2728
import co.elastic.apm.agent.impl.MetaData;
2829
import co.elastic.apm.agent.impl.context.AbstractContext;
2930
import co.elastic.apm.agent.impl.context.Db;
@@ -58,6 +59,7 @@
5859
import co.elastic.apm.agent.metrics.MetricRegistry;
5960
import co.elastic.apm.agent.metrics.MetricSet;
6061
import co.elastic.apm.agent.report.ApmServerClient;
62+
import co.elastic.apm.agent.util.HexUtils;
6163
import co.elastic.apm.agent.util.PotentiallyMultiValuedMap;
6264
import com.dslplatform.json.BoolConverter;
6365
import com.dslplatform.json.DslJson;
@@ -85,6 +87,7 @@
8587
import static com.dslplatform.json.JsonWriter.COMMA;
8688
import static com.dslplatform.json.JsonWriter.OBJECT_END;
8789
import static com.dslplatform.json.JsonWriter.OBJECT_START;
90+
import static com.dslplatform.json.JsonWriter.QUOTE;
8891

8992
public class DslJsonSerializer implements PayloadSerializer, MetricRegistry.MetricsReporter {
9093

@@ -537,6 +540,7 @@ private void serializeSpan(final Span span) {
537540
serializeStackTrace(span.getStackFrames());
538541
}
539542
serializeSpanContext(span.getContext(), span.getTraceContext());
543+
writeHexArray("child_ids", span.getChildIds());
540544
serializeSpanType(span);
541545
jw.writeByte(OBJECT_END);
542546
}
@@ -1258,4 +1262,21 @@ private void writeTimestamp(final long epochMicros) {
12581262
NumberConverter.serialize(epochMicros, jw);
12591263
jw.writeByte(COMMA);
12601264
}
1265+
1266+
private void writeHexArray(String fieldName, @Nullable LongList longList) {
1267+
if (longList != null && longList.getSize() > 0) {
1268+
writeFieldName(fieldName);
1269+
jw.writeByte(ARRAY_START);
1270+
for (int i = 0, size = longList.getSize(); i < size; i++) {
1271+
if (i > 0) {
1272+
jw.writeByte(COMMA);
1273+
}
1274+
jw.writeByte(QUOTE);
1275+
HexUtils.writeAsHex(longList.get(i), jw);
1276+
jw.writeByte(QUOTE);
1277+
}
1278+
jw.writeByte(ARRAY_END);
1279+
jw.writeByte(COMMA);
1280+
}
1281+
}
12611282
}

0 commit comments

Comments
 (0)