Skip to content

Commit 1f1857b

Browse files
authored
Universal profiling integration: Added serialization of stacktrace IDs as profiler_stack_trace_ids otel attributes (#3607)
1 parent b14f658 commit 1f1857b

File tree

7 files changed

+199
-4
lines changed

7 files changed

+199
-4
lines changed

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@
3434
import co.elastic.apm.agent.impl.metadata.MetaDataFuture;
3535
import co.elastic.apm.agent.impl.metadata.NameAndIdField;
3636
import co.elastic.apm.agent.impl.metadata.ServiceFactory;
37+
import co.elastic.apm.agent.impl.transaction.Id;
3738
import co.elastic.apm.agent.sdk.internal.util.LoggerUtils;
3839
import co.elastic.apm.agent.tracer.metrics.DoubleSupplier;
3940
import co.elastic.apm.agent.tracer.metrics.Labels;
41+
import co.elastic.apm.agent.tracer.pooling.Allocator;
4042
import co.elastic.apm.agent.tracer.service.Service;
4143
import co.elastic.apm.agent.tracer.service.ServiceInfo;
4244
import co.elastic.apm.agent.configuration.SpanConfiguration;
@@ -127,6 +129,7 @@ public class ElasticApmTracer implements Tracer {
127129
private final ObjectPool<Span> spanPool;
128130
private final ObjectPool<ErrorCapture> errorPool;
129131
private final ObjectPool<TraceContext> spanLinkPool;
132+
private final ObjectPool<Id> profilingCorrelationStackTraceIdPool;
130133
private final Reporter reporter;
131134
private final ObjectPoolFactory objectPoolFactory;
132135

@@ -245,6 +248,13 @@ public void onChange(ConfigurationOption<?> configurationOption, Boolean oldValu
245248
// span links pool allows for 10X the maximum allowed span links per span
246249
spanLinkPool = poolFactory.createSpanLinkPool(AbstractSpan.MAX_ALLOWED_SPAN_LINKS * 10, this);
247250

251+
profilingCorrelationStackTraceIdPool = poolFactory.createRecyclableObjectPool(maxPooledElements, new Allocator<Id>() {
252+
@Override
253+
public Id createInstance() {
254+
return Id.new128BitId();
255+
}
256+
});
257+
248258
sampler = ProbabilitySampler.of(coreConfiguration.getSampleRate().get());
249259
coreConfiguration.getSampleRate().addChangeListener(new ConfigurationOption.ChangeListener<Double>() {
250260
@Override
@@ -604,6 +614,10 @@ public TraceContext createSpanLink() {
604614
return spanLinkPool.createInstance();
605615
}
606616

617+
public Id createProfilingCorrelationStackTraceId() {
618+
return profilingCorrelationStackTraceIdPool.createInstance();
619+
}
620+
607621
public void recycle(Transaction transaction) {
608622
transactionPool.recycle(transaction);
609623
}
@@ -620,6 +634,10 @@ public void recycle(TraceContext traceContext) {
620634
spanLinkPool.recycle(traceContext);
621635
}
622636

637+
public void recycleProfilingCorrelationStackTraceId(Id id) {
638+
profilingCorrelationStackTraceIdPool.recycle(id);
639+
}
640+
623641
public synchronized void stop() {
624642
if (tracerState == TracerState.STOPPED) {
625643
// may happen if explicitly stopped in a unit test and executed again within a shutdown hook

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919
package co.elastic.apm.agent.impl.transaction;
2020

21+
import co.elastic.apm.agent.report.serialize.Base64SerializationUtils;
2122
import co.elastic.apm.agent.report.serialize.HexSerializationUtils;
2223
import co.elastic.apm.agent.tracer.pooling.Recyclable;
2324
import co.elastic.apm.agent.tracer.util.HexUtils;
@@ -175,6 +176,10 @@ public void writeAsHex(JsonWriter jw) {
175176
HexSerializationUtils.writeBytesAsHex(data, jw);
176177
}
177178

179+
public void writeAsBase64UrlSafe(JsonWriter jw) {
180+
Base64SerializationUtils.writeBytesAsBase64UrlSafe(data, jw);
181+
}
182+
178183
public void writeAsHex(StringBuilder sb) {
179184
HexUtils.writeBytesAsHex(data, sb);
180185
}

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.HdrHistogram.WriterReaderPhaser;
3838

3939
import javax.annotation.Nullable;
40+
import java.util.ArrayList;
4041
import java.util.List;
4142
import java.util.concurrent.atomic.AtomicBoolean;
4243

@@ -107,6 +108,8 @@ public class Transaction extends AbstractSpan<Transaction> implements co.elastic
107108
@Nullable
108109
private Throwable pendingException;
109110

111+
private final ArrayList<Id> profilingCorrelationStackTraceIds = new ArrayList<>();
112+
110113
/**
111114
* Faas
112115
* <p>
@@ -341,9 +344,23 @@ public void resetState() {
341344
faas.resetState();
342345
wasActivated.set(false);
343346
pendingException = null;
347+
recycleProfilingCorrelationStackTraceIds();
344348
// don't clear timerBySpanTypeAndSubtype map (see field-level javadoc)
345349
}
346350

351+
private void recycleProfilingCorrelationStackTraceIds() {
352+
for (Id toRecycle : profilingCorrelationStackTraceIds) {
353+
tracer.recycleProfilingCorrelationStackTraceId(toRecycle);
354+
}
355+
if (profilingCorrelationStackTraceIds.size() > 100) {
356+
profilingCorrelationStackTraceIds.clear();
357+
//trim overly big lists
358+
profilingCorrelationStackTraceIds.trimToSize();
359+
} else {
360+
profilingCorrelationStackTraceIds.clear();
361+
}
362+
}
363+
347364
@Override
348365
public boolean isNoop() {
349366
return noop;
@@ -552,4 +569,19 @@ public Throwable getPendingTransactionException() {
552569
return this.pendingException;
553570
}
554571

572+
public void addProfilerCorrelationStackTrace(Id idToCopy) {
573+
Id id = tracer.createProfilingCorrelationStackTraceId();
574+
id.copyFrom(idToCopy);
575+
synchronized (profilingCorrelationStackTraceIds) {
576+
this.profilingCorrelationStackTraceIds.add(id);
577+
}
578+
}
579+
580+
/**
581+
* Returns the list of stacktrace-IDs from the profiler associated with this transaction
582+
* To protect agains concurrent modifications, consumers must synchronize on the returned list.
583+
*/
584+
public List<Id> getProfilingCorrelationStackTraceIds() {
585+
return profilingCorrelationStackTraceIds;
586+
}
555587
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package co.elastic.apm.agent.report.serialize;
2+
3+
import com.dslplatform.json.JsonWriter;
4+
5+
public class Base64SerializationUtils {
6+
7+
private static final byte[] BASE64_URL_CHARS = new byte[]{
8+
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
9+
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
10+
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
11+
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
12+
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
13+
'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
14+
'w', 'x', 'y', 'z', '0', '1', '2', '3',
15+
'4', '5', '6', '7', '8', '9', '-', '_',
16+
};
17+
18+
19+
public static void writeBytesAsBase64UrlSafe(byte[] data, JsonWriter jw) {
20+
int i = 0;
21+
for (; i + 2 < data.length; i += 3) {
22+
int b0 = ((int) data[i]) & 0xFF;
23+
int b1 = ((int) data[i + 1]) & 0xFF;
24+
int b2 = ((int) data[i + 2]) & 0xFF;
25+
jw.writeByte(BASE64_URL_CHARS[b0 >> 2]);
26+
jw.writeByte(BASE64_URL_CHARS[((b0 << 4) & 63) | (b1 >> 4)]);
27+
jw.writeByte(BASE64_URL_CHARS[((b1 << 2) & 63) | (b2 >> 6)]);
28+
jw.writeByte(BASE64_URL_CHARS[b2 & 63]);
29+
}
30+
int leftOver = data.length - i;
31+
if (leftOver == 1) {
32+
int b0 = ((int) data[i]) & 0xFF;
33+
jw.writeByte(BASE64_URL_CHARS[b0 >> 2]);
34+
jw.writeByte(BASE64_URL_CHARS[(b0 << 4) & 63]);
35+
} else if (leftOver == 2) {
36+
int b0 = ((int) data[i]) & 0xFF;
37+
int b1 = ((int) data[i + 1]) & 0xFF;
38+
jw.writeByte(BASE64_URL_CHARS[b0 >> 2]);
39+
jw.writeByte(BASE64_URL_CHARS[((b0 << 4) & 63) | (b1 >> 4)]);
40+
jw.writeByte(BASE64_URL_CHARS[(b1 << 2) & 63]);
41+
}
42+
}
43+
}

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

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
import java.nio.charset.StandardCharsets;
8383
import java.util.ArrayList;
8484
import java.util.Arrays;
85+
import java.util.Collections;
8586
import java.util.Iterator;
8687
import java.util.List;
8788
import java.util.Map;
@@ -1050,10 +1051,22 @@ private void serializeSpanLinks(List<TraceContext> spanLinks) {
10501051
}
10511052
}
10521053

1053-
private void serializeOTel(AbstractSpan<?> span) {
1054+
private void serializeOTel(Span span) {
1055+
serializeOtel(span, Collections.<Id>emptyList());
1056+
}
1057+
1058+
private void serializeOTel(Transaction transaction) {
1059+
List<Id> profilingCorrelationStackTraceIds = transaction.getProfilingCorrelationStackTraceIds();
1060+
synchronized (profilingCorrelationStackTraceIds) {
1061+
serializeOtel(transaction, profilingCorrelationStackTraceIds);
1062+
}
1063+
}
1064+
1065+
private void serializeOtel(AbstractSpan<?> span, List<Id> profilingStackTraceIds) {
10541066
OTelSpanKind kind = span.getOtelKind();
10551067
Map<String, Object> attributes = span.getOtelAttributes();
1056-
boolean hasAttributes = !attributes.isEmpty();
1068+
1069+
boolean hasAttributes = !attributes.isEmpty() || !profilingStackTraceIds.isEmpty();
10571070
boolean hasKind = kind != null;
10581071
if (hasKind || hasAttributes) {
10591072
writeFieldName("otel");
@@ -1070,11 +1083,13 @@ private void serializeOTel(AbstractSpan<?> span) {
10701083
}
10711084
writeFieldName("attributes");
10721085
jw.writeByte(OBJECT_START);
1073-
int index = 0;
1086+
boolean isFirstAttrib = true;
10741087
for (Map.Entry<String, Object> entry : attributes.entrySet()) {
1075-
if (index++ > 0) {
1088+
if (!isFirstAttrib) {
10761089
jw.writeByte(COMMA);
10771090
}
1091+
isFirstAttrib = false;
1092+
10781093
writeFieldName(entry.getKey());
10791094
Object o = entry.getValue();
10801095
if (o instanceof Number) {
@@ -1085,6 +1100,22 @@ private void serializeOTel(AbstractSpan<?> span) {
10851100
BoolConverter.serialize((Boolean) o, jw);
10861101
}
10871102
}
1103+
if (!profilingStackTraceIds.isEmpty()) {
1104+
if (!isFirstAttrib) {
1105+
jw.writeByte(COMMA);
1106+
}
1107+
writeFieldName("elastic.profiler_stack_trace_ids");
1108+
jw.writeByte(ARRAY_START);
1109+
for (int i = 0; i < profilingStackTraceIds.size(); i++) {
1110+
if (i != 0) {
1111+
jw.writeByte(COMMA);
1112+
}
1113+
jw.writeByte(QUOTE);
1114+
profilingStackTraceIds.get(i).writeAsBase64UrlSafe(jw);
1115+
jw.writeByte(QUOTE);
1116+
}
1117+
jw.writeByte(ARRAY_END);
1118+
}
10881119
jw.writeByte(OBJECT_END);
10891120
}
10901121

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package co.elastic.apm.agent.report.serialize;
2+
3+
import com.dslplatform.json.DslJson;
4+
import com.dslplatform.json.JsonWriter;
5+
import org.junit.jupiter.api.Test;
6+
7+
import java.util.Base64;
8+
import java.util.Random;
9+
10+
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
11+
12+
public class Base64SerializationUtilTest {
13+
14+
@Test
15+
public void empty() {
16+
JsonWriter jw = new DslJson<>(new DslJson.Settings<>()).newWriter();
17+
Base64SerializationUtils.writeBytesAsBase64UrlSafe(new byte[0], jw);
18+
assertThat(jw.size()).isEqualTo(0);
19+
}
20+
21+
@Test
22+
public void randomInputs() {
23+
DslJson<Object> dslJson = new DslJson<>(new DslJson.Settings<>());
24+
25+
Base64.Encoder reference = Base64.getUrlEncoder().withoutPadding();
26+
27+
Random rnd = new Random(42);
28+
for (int i = 0; i < 100_000; i++) {
29+
int len = rnd.nextInt(31) + 1;
30+
byte[] data = new byte[len];
31+
rnd.nextBytes(data);
32+
33+
String expectedResult = reference.encodeToString(data);
34+
35+
JsonWriter jw = dslJson.newWriter();
36+
Base64SerializationUtils.writeBytesAsBase64UrlSafe(data, jw);
37+
38+
assertThat(jw.toString()).isEqualTo(expectedResult);
39+
}
40+
}
41+
}

apm-agent-core/src/test/java/co/elastic/apm/agent/report/serialize/DslJsonSerializerTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1584,6 +1584,31 @@ void testSpanLinksSerialization() {
15841584
assertThat(parent2link.get("span_id").textValue()).isEqualTo(parent2.getTraceContext().getId().toString());
15851585
}
15861586

1587+
private static Id create128BitId(String id) {
1588+
Id idObj = Id.new128BitId();
1589+
idObj.fromHexString(id, 0);
1590+
return idObj;
1591+
}
1592+
1593+
@Test
1594+
void testProfilingStackTraceIdSerialization() {
1595+
Transaction transaction = tracer.startRootTransaction(null);
1596+
1597+
transaction.addProfilerCorrelationStackTrace(create128BitId("a1a2a3a4a5a6a7a8b1b2b3b4b5b6b7b8"));
1598+
transaction.addProfilerCorrelationStackTrace(create128BitId("c1c2c3c4c5c6c7c8d1d2d3d4d5d6d7d8"));
1599+
1600+
JsonNode transactionJson = readJsonString(writer.toJsonString(transaction));
1601+
JsonNode otel = transactionJson.get("otel");
1602+
assertThat(otel).isNotNull();
1603+
JsonNode attributes = otel.get("attributes");
1604+
assertThat(attributes).isNotNull();
1605+
JsonNode ids = attributes.get("elastic.profiler_stack_trace_ids");
1606+
assertThat(ids.isArray()).isTrue();
1607+
assertThat(ids.size()).isEqualTo(2);
1608+
assertThat(ids.get(0).asText()).isEqualTo("oaKjpKWmp6ixsrO0tba3uA");
1609+
assertThat(ids.get(1).asText()).isEqualTo("wcLDxMXGx8jR0tPU1dbX2A");
1610+
}
1611+
15871612
@ParameterizedTest
15881613
@ValueSource(booleans = {true, false})
15891614
void testSerializeLog(boolean asString) {

0 commit comments

Comments
 (0)