Skip to content

Commit 49c863e

Browse files
marcsanmiCopilot
andauthored
feat: add setTraceId and clearTraceId to ProfilerApi (#313)
* feat: add setTraceId and clearTraceId to ProfilerApi * refactor: route setTraceId through the native AsyncProfiler API * chore: bump async-profiler to 4.4.0.0 (adds native setTraceId) * perf: parse trace_id hex without allocating substrings * test: cover trace_id hex parser cases * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: validate trace_id length up front and fail with a clear exception --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent e41b512 commit 49c863e

11 files changed

Lines changed: 105 additions & 4 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
GRAFANA_PYROSCOPE_VERSION := 4.3.0.1
1+
GRAFANA_PYROSCOPE_VERSION := 4.4.0.0
22

33
.PHONY: download-async-profiler
44
download-async-profiler:

agent/src/main/java/io/pyroscope/javaagent/ProfilerSdk.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,40 @@ public void setTracingContext(long spanId, long spanName) {
4646
public long registerConstant(String constant) {
4747
return Pyroscope.LabelsWrapper.registerConstant(constant);
4848
}
49+
50+
@Override
51+
public void setTraceId(@NotNull String traceId) {
52+
// W3C trace ID is 32 hex chars (128 bits). Parse directly into two longs
53+
// to avoid the String#substring allocations on this hot path.
54+
if (traceId.length() != 32) {
55+
throw new NumberFormatException("trace_id must be 32 hex chars, got length " + traceId.length());
56+
}
57+
long hi = parseHex64(traceId, 0);
58+
long lo = parseHex64(traceId, 16);
59+
asprof.setTraceId(hi, lo);
60+
}
61+
62+
@Override
63+
public void clearTraceId() {
64+
asprof.setTraceId(0L, 0L);
65+
}
66+
67+
static long parseHex64(String s, int offset) {
68+
long result = 0L;
69+
for (int i = 0; i < 16; i++) {
70+
int c = s.charAt(offset + i);
71+
int nibble;
72+
if (c >= '0' && c <= '9') {
73+
nibble = c - '0';
74+
} else if (c >= 'a' && c <= 'f') {
75+
nibble = c - 'a' + 10;
76+
} else if (c >= 'A' && c <= 'F') {
77+
nibble = c - 'A' + 10;
78+
} else {
79+
throw new NumberFormatException("invalid hex char in trace_id at index " + (offset + i));
80+
}
81+
result = (result << 4) | nibble;
82+
}
83+
return result;
84+
}
4985
}

agent/src/main/java/io/pyroscope/javaagent/api/ProfilerApi.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,15 @@ public interface ProfilerApi {
2323
void setTracingContext(long spanId, long spanName);
2424

2525
long registerConstant(String constant);
26+
27+
/**
28+
* Attaches a {@code trace_id} label to samples produced by the current thread.
29+
* Thread local; pair with {@link #clearTraceId()}.
30+
*/
31+
// Default methods so an older ProfilerApi implementation can run against a newer ProfilerApi
32+
// injected into the bootstrap classloader (no label rather than AbstractMethodError).
33+
// Note: this does not help if the runtime ProfilerApi itself is older and lacks this method.
34+
default void setTraceId(@NotNull String traceId) {}
35+
36+
default void clearTraceId() {}
2637
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package io.pyroscope.javaagent;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import static org.junit.jupiter.api.Assertions.assertEquals;
6+
import static org.junit.jupiter.api.Assertions.assertThrows;
7+
8+
class ProfilerSdkTest {
9+
10+
@Test
11+
void parseHex64_lowercase() {
12+
// 0x0123456789abcdef
13+
assertEquals(0x0123456789abcdefL, ProfilerSdk.parseHex64("0123456789abcdef", 0));
14+
}
15+
16+
@Test
17+
void parseHex64_uppercase() {
18+
// OTel emits lowercase per W3C spec, but defensive: accept uppercase too.
19+
assertEquals(0x0123456789ABCDEFL, ProfilerSdk.parseHex64("0123456789ABCDEF", 0));
20+
}
21+
22+
@Test
23+
void parseHex64_allZeros() {
24+
assertEquals(0L, ProfilerSdk.parseHex64("0000000000000000", 0));
25+
}
26+
27+
@Test
28+
void parseHex64_allFs() {
29+
assertEquals(-1L, ProfilerSdk.parseHex64("ffffffffffffffff", 0));
30+
}
31+
32+
@Test
33+
void parseHex64_offset() {
34+
// Parse the low half of a full 32 char trace id.
35+
String traceId = "0123456789abcdeffedcba9876543210";
36+
assertEquals(0x0123456789abcdefL, ProfilerSdk.parseHex64(traceId, 0));
37+
assertEquals(0xfedcba9876543210L, ProfilerSdk.parseHex64(traceId, 16));
38+
}
39+
40+
@Test
41+
void parseHex64_invalidChar() {
42+
assertThrows(NumberFormatException.class, () -> ProfilerSdk.parseHex64("0123456789abcdez", 0));
43+
}
44+
45+
@Test
46+
void setTraceId_rejectsWrongLength() {
47+
ProfilerSdk sdk = new ProfilerSdk();
48+
// Both shorter and longer than 32 must throw the same exception type.
49+
assertThrows(NumberFormatException.class, () -> sdk.setTraceId(""));
50+
assertThrows(NumberFormatException.class, () -> sdk.setTraceId("0123456789abcdef"));
51+
assertThrows(NumberFormatException.class, () ->
52+
sdk.setTraceId("0123456789abcdef0123456789abcdef00"));
53+
}
54+
}
34 Bytes
Binary file not shown.
Binary file not shown.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
8327292d14d9513a296a208c022adf01a1b0ca3c
1+
0cb3705aa2c9632dc2a0bb3344ce8e6532bc0f78
-3.93 KB
Binary file not shown.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
9c277b06ac6ffc80c9225dbef119f1e2e82f829b
1+
84373075b3ab8f713c0f265220da60d96d363296
16.1 KB
Binary file not shown.

0 commit comments

Comments
 (0)