Skip to content

Commit 242ca41

Browse files
Profiling - OTEL profiling fix, Stabilization, Logging (#4746)
* add skipProfiling flag to TransactionOptions to be able to skip profiling and handle cases where profiling has been started by otel * add profilerId to spanContext so that otel span processor can propagate this to the exporter and SentryTracer * immediately end profiling when stopProfiler is called * bump api, fix android api 24 code * catch all exception happening when converting from jfr * simplify JavaContinuous profiler by catching AsyncProfiler instantiation exceptions in provider * add exists and writable info to log message * add method to safely delete file * remove setNative call * fix test * fix reference to commit we vendored from * drop event if it cannot be processed to not lose the whole chunk * Format code * fix test * Format code * fix test * catch exceptions in startProfiler/stopProfiler * fallback to threadId -1 if it cannot be resolved --------- Co-authored-by: Sentry Github Bot <[email protected]>
1 parent 2e54dde commit 242ca41

File tree

10 files changed

+93
-72
lines changed

10 files changed

+93
-72
lines changed

sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverter.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
public final class JfrAsyncProfilerToSentryProfileConverter extends JfrConverter {
2828
private static final double NANOS_PER_SECOND = 1_000_000_000.0;
29+
private static final long UNKNOWN_THREAD_ID = -1;
2930

3031
private final @NotNull SentryProfile sentryProfile = new SentryProfile();
3132
private final @NotNull SentryStackTraceFactory stackTraceFactory;
@@ -113,13 +114,16 @@ public void visit(Event event, long samples, long value) {
113114
}
114115
}
115116

116-
private long resolveThreadId(int eventThreadId) {
117-
return jfr.threads.get(eventThreadId) != null
118-
? jfr.javaThreads.get(eventThreadId)
119-
: eventThreadId;
117+
private long resolveThreadId(int eventId) {
118+
Long javaThreadId = jfr.javaThreads.get(eventId);
119+
return javaThreadId != null ? javaThreadId : UNKNOWN_THREAD_ID;
120120
}
121121

122122
private void processThreadMetadata(Event event, long threadId) {
123+
if (threadId == UNKNOWN_THREAD_ID) {
124+
return;
125+
}
126+
123127
final String threadName = getPlainThreadName(event.tid);
124128
sentryProfile
125129
.getThreadMetadata()

sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfiler.java

Lines changed: 21 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,8 @@ public final class JavaContinuousProfiler
5656

5757
private @NotNull String filename = "";
5858

59-
private @Nullable AsyncProfiler profiler;
59+
private @NotNull AsyncProfiler profiler;
6060
private volatile boolean shouldSample = true;
61-
private boolean shouldStop = false;
6261
private boolean isSampled = false;
6362
private int rootSpanCounter = 0;
6463

@@ -69,27 +68,20 @@ public JavaContinuousProfiler(
6968
final @NotNull ILogger logger,
7069
final @Nullable String profilingTracesDirPath,
7170
final int profilingTracesHz,
72-
final @NotNull ISentryExecutorService executorService) {
71+
final @NotNull ISentryExecutorService executorService)
72+
throws Exception {
7373
this.logger = logger;
7474
this.profilingTracesDirPath = profilingTracesDirPath;
7575
this.profilingTracesHz = profilingTracesHz;
7676
this.executorService = executorService;
7777
initializeProfiler();
7878
}
7979

80-
private void initializeProfiler() {
81-
try {
82-
this.profiler = AsyncProfiler.getInstance();
83-
// Check version to verify profiler is working
84-
String version = profiler.execute("version");
85-
logger.log(SentryLevel.DEBUG, "AsyncProfiler initialized successfully. Version: " + version);
86-
} catch (Exception e) {
87-
logger.log(
88-
SentryLevel.WARNING,
89-
"Failed to initialize AsyncProfiler. Profiling will be disabled.",
90-
e);
91-
this.profiler = null;
92-
}
80+
private void initializeProfiler() throws Exception {
81+
this.profiler = AsyncProfiler.getInstance();
82+
// Check version to verify profiler is working
83+
String version = profiler.execute("version");
84+
logger.log(SentryLevel.DEBUG, "AsyncProfiler initialized successfully. Version: " + version);
9385
}
9486

9587
private boolean init() {
@@ -98,11 +90,6 @@ private boolean init() {
9890
}
9991
isInitialized = true;
10092

101-
if (profiler == null) {
102-
logger.log(SentryLevel.ERROR, "Disabling profiling because AsyncProfiler is not available.");
103-
return false;
104-
}
105-
10693
if (profilingTracesDirPath == null) {
10794
logger.log(
10895
SentryLevel.WARNING,
@@ -168,10 +155,11 @@ public void startProfiler(
168155
}
169156

170157
if (!isRunning()) {
171-
shouldStop = false;
172158
logger.log(SentryLevel.DEBUG, "Started Profiler.");
173159
start();
174160
}
161+
} catch (Exception e) {
162+
logger.log(SentryLevel.ERROR, "Error starting profiler: ", e);
175163
}
176164
}
177165

@@ -210,11 +198,6 @@ private void start() {
210198
startProfileChunkTimestamp = new SentryNanotimeDate();
211199
}
212200

213-
if (profiler == null) {
214-
logger.log(SentryLevel.ERROR, "Cannot start profiling: AsyncProfiler is not available");
215-
return;
216-
}
217-
218201
filename = profilingTracesDirPath + File.separator + SentryUUID.generateSentryId() + ".jfr";
219202

220203
File jfrFile = new File(filename);
@@ -250,7 +233,8 @@ private void start() {
250233
SentryLevel.ERROR,
251234
"Failed to schedule profiling chunk finish. Did you call Sentry.close()?",
252235
e);
253-
shouldStop = true;
236+
// If we can't schedule the auto-stop, stop immediately without restart
237+
stop(false);
254238
}
255239
}
256240

@@ -269,10 +253,12 @@ public void stopProfiler(final @NotNull ProfileLifecycle profileLifecycle) {
269253
if (rootSpanCounter < 0) {
270254
rootSpanCounter = 0;
271255
}
272-
shouldStop = true;
256+
// Stop immediately without restart
257+
stop(false);
273258
break;
274259
case MANUAL:
275-
shouldStop = true;
260+
// Stop immediately without restart
261+
stop(false);
276262
break;
277263
}
278264
}
@@ -293,11 +279,6 @@ private void stop(final boolean restartProfiler) {
293279

294280
File jfrFile = new File(filename);
295281

296-
if (profiler == null) {
297-
logger.log(SentryLevel.WARNING, "Profiler is null when trying to stop");
298-
return;
299-
}
300-
301282
try {
302283
profiler.execute("stop,jfr");
303284
} catch (Exception e) {
@@ -339,14 +320,16 @@ private void stop(final boolean restartProfiler) {
339320
sendChunks(scopes, scopes.getOptions());
340321
}
341322

342-
if (restartProfiler && !shouldStop) {
323+
if (restartProfiler) {
343324
logger.log(SentryLevel.DEBUG, "Profile chunk finished. Starting a new one.");
344325
start();
345326
} else {
346327
// When the profiler is stopped manually, we have to reset its id
347328
profilerId = SentryId.EMPTY_ID;
348329
logger.log(SentryLevel.DEBUG, "Profile chunk finished.");
349330
}
331+
} catch (Exception e) {
332+
logger.log(SentryLevel.ERROR, "Error stopping profiler: ", e);
350333
}
351334
}
352335

@@ -359,9 +342,8 @@ public void reevaluateSampling() {
359342
public void close(final boolean isTerminating) {
360343
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
361344
rootSpanCounter = 0;
362-
shouldStop = true;
345+
stop(false);
363346
if (isTerminating) {
364-
stop(false);
365347
isClosed.set(true);
366348
}
367349
}
@@ -412,7 +394,7 @@ private void safelyRemoveFile(File file) {
412394
@Override
413395
public boolean isRunning() {
414396
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
415-
return isRunning && profiler != null && !filename.isEmpty();
397+
return isRunning && !filename.isEmpty();
416398
}
417399
}
418400

sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerContinuousProfilerProvider.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import io.sentry.IContinuousProfiler;
44
import io.sentry.ILogger;
55
import io.sentry.ISentryExecutorService;
6+
import io.sentry.NoOpContinuousProfiler;
7+
import io.sentry.SentryLevel;
68
import io.sentry.asyncprofiler.profiling.JavaContinuousProfiler;
79
import io.sentry.profiling.JavaContinuousProfilerProvider;
810
import io.sentry.profiling.JavaProfileConverterProvider;
@@ -22,7 +24,15 @@ public final class AsyncProfilerContinuousProfilerProvider
2224
String profilingTracesDirPath,
2325
int profilingTracesHz,
2426
ISentryExecutorService executorService) {
25-
return new JavaContinuousProfiler(
26-
logger, profilingTracesDirPath, profilingTracesHz, executorService);
27+
try {
28+
return new JavaContinuousProfiler(
29+
logger, profilingTracesDirPath, profilingTracesHz, executorService);
30+
} catch (Exception e) {
31+
logger.log(
32+
SentryLevel.WARNING,
33+
"Failed to initialize AsyncProfiler. Profiling will be disabled.",
34+
e);
35+
return NoOpContinuousProfiler.getInstance();
36+
}
2737
}
2838
}

sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfilerTest.kt

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,6 @@ class JavaContinuousProfilerTest {
117117
assertTrue(profiler.isRunning)
118118
// We are scheduling the profiler to stop at the end of the chunk, so it should still be running
119119
profiler.stopProfiler(ProfileLifecycle.MANUAL)
120-
assertTrue(profiler.isRunning)
121-
// We run the executor service to trigger the chunk finish, and the profiler shouldn't restart
122-
fixture.executor.runAll()
123120
assertFalse(profiler.isRunning)
124121
}
125122

@@ -325,27 +322,24 @@ class JavaContinuousProfilerTest {
325322
assertTrue(profiler.isRunning)
326323
// We run the executor service to trigger the profiler restart (chunk finish)
327324
fixture.executor.runAll()
325+
// At this point the chunk has been submitted to the executor, but yet to be sent
328326
verify(fixture.scopes, never()).captureProfileChunk(any())
329327
profiler.stopProfiler(ProfileLifecycle.MANUAL)
330-
// We stop the profiler, which should send a chunk
328+
// We stop the profiler, which should send both the first and last chunk
331329
fixture.executor.runAll()
332-
verify(fixture.scopes).captureProfileChunk(any())
330+
verify(fixture.scopes, times(2)).captureProfileChunk(any())
333331
}
334332

335333
@Test
336334
fun `close without terminating stops all profiles after chunk is finished`() {
337335
val profiler = fixture.getSut()
338336
profiler.startProfiler(ProfileLifecycle.TRACE, fixture.mockTracesSampler)
339337
assertTrue(profiler.isRunning)
340-
// We are scheduling the profiler to stop at the end of the chunk, so it should still be running
338+
// We are closing the profiler, which should stop all profiles after the chunk is finished
341339
profiler.close(false)
342-
assertTrue(profiler.isRunning)
340+
assertFalse(profiler.isRunning)
343341
// However, close() already resets the rootSpanCounter
344342
assertEquals(0, profiler.rootSpanCounter)
345-
346-
// We run the executor service to trigger the chunk finish, and the profiler shouldn't restart
347-
fixture.executor.runAll()
348-
assertFalse(profiler.isRunning)
349343
}
350344

351345
@Test

sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ public void onStart(final @NotNull Context parentContext, final @NotNull ReadWri
117117
sentryParentSpanId,
118118
baggage);
119119
sentrySpan.getSpanContext().setOrigin(SentrySpanExporter.TRACE_ORIGIN);
120+
sentrySpan
121+
.getSpanContext()
122+
.setProfilerId(scopes.getOptions().getContinuousProfiler().getProfilerId());
120123
spanStorage.storeSentrySpan(spanContext, sentrySpan);
121124
}
122125

sentry/api/sentry.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3967,6 +3967,7 @@ public class io/sentry/SpanContext : io/sentry/JsonSerializable, io/sentry/JsonU
39673967
public fun getOrigin ()Ljava/lang/String;
39683968
public fun getParentSpanId ()Lio/sentry/SpanId;
39693969
public fun getProfileSampled ()Ljava/lang/Boolean;
3970+
public fun getProfilerId ()Lio/sentry/protocol/SentryId;
39703971
public fun getSampled ()Ljava/lang/Boolean;
39713972
public fun getSamplingDecision ()Lio/sentry/TracesSamplingDecision;
39723973
public fun getSpanId ()Lio/sentry/SpanId;
@@ -3981,6 +3982,7 @@ public class io/sentry/SpanContext : io/sentry/JsonSerializable, io/sentry/JsonU
39813982
public fun setInstrumenter (Lio/sentry/Instrumenter;)V
39823983
public fun setOperation (Ljava/lang/String;)V
39833984
public fun setOrigin (Ljava/lang/String;)V
3985+
public fun setProfilerId (Lio/sentry/protocol/SentryId;)V
39843986
public fun setSampled (Ljava/lang/Boolean;)V
39853987
public fun setSampled (Ljava/lang/Boolean;Ljava/lang/Boolean;)V
39863988
public fun setSamplingDecision (Lio/sentry/TracesSamplingDecision;)V

sentry/src/main/java/io/sentry/Scopes.java

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -938,17 +938,18 @@ public void flush(long timeoutMillis) {
938938
final @NotNull ISpanFactory spanFactory =
939939
maybeSpanFactory == null ? getOptions().getSpanFactory() : maybeSpanFactory;
940940

941-
// If continuous profiling is enabled in trace mode, let's start it. Profiler will sample on
942-
// its own.
941+
// If continuous profiling is enabled in trace mode, let's start it unless skipProfiling is
942+
// true in TransactionOptions.
943+
// Profiler will sample on its own.
943944
// Profiler is started before the transaction is created, so that the profiler id is available
944945
// when the transaction starts
945-
if (samplingDecision.getSampled()) {
946-
if (getOptions().isContinuousProfilingEnabled()
947-
&& getOptions().getProfileLifecycle() == ProfileLifecycle.TRACE) {
948-
getOptions()
949-
.getContinuousProfiler()
950-
.startProfiler(ProfileLifecycle.TRACE, getOptions().getInternalTracesSampler());
951-
}
946+
if (samplingDecision.getSampled()
947+
&& getOptions().isContinuousProfilingEnabled()
948+
&& getOptions().getProfileLifecycle() == ProfileLifecycle.TRACE
949+
&& transactionContext.getProfilerId().equals(SentryId.EMPTY_ID)) {
950+
getOptions()
951+
.getContinuousProfiler()
952+
.startProfiler(ProfileLifecycle.TRACE, getOptions().getInternalTracesSampler());
952953
}
953954

954955
transaction =

sentry/src/main/java/io/sentry/SentryTracer.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ public SentryTracer(
8383

8484
setDefaultSpanData(root);
8585

86-
final @NotNull SentryId continuousProfilerId =
87-
scopes.getOptions().getContinuousProfiler().getProfilerId();
86+
final @NotNull SentryId continuousProfilerId = getProfilerId();
87+
8888
if (!continuousProfilerId.equals(SentryId.EMPTY_ID) && Boolean.TRUE.equals(isSampled())) {
8989
this.contexts.setProfile(new ProfileContext(continuousProfilerId));
9090
}
@@ -229,7 +229,7 @@ public void finish(
229229
}
230230
});
231231

232-
// any un-finished childs will remain unfinished
232+
// any un-finished children will remain unfinished
233233
// as relay takes care of setting the end-timestamp + deadline_exceeded
234234
// see
235235
// https://github.com/getsentry/relay/blob/40697d0a1c54e5e7ad8d183fc7f9543b94fe3839/relay-general/src/store/transactions/processor.rs#L374-L378
@@ -244,7 +244,8 @@ public void finish(
244244
.onTransactionFinish(this, performanceCollectionData.get(), scopes.getOptions());
245245
}
246246
if (scopes.getOptions().isContinuousProfilingEnabled()
247-
&& scopes.getOptions().getProfileLifecycle() == ProfileLifecycle.TRACE) {
247+
&& scopes.getOptions().getProfileLifecycle() == ProfileLifecycle.TRACE
248+
&& root.getSpanContext().getProfilerId().equals(SentryId.EMPTY_ID)) {
248249
scopes.getOptions().getContinuousProfiler().stopProfiler(ProfileLifecycle.TRACE);
249250
}
250251
if (performanceCollectionData.get() != null) {
@@ -543,8 +544,7 @@ private ISpan createChild(
543544
/** Sets the default data in the span, including profiler _id, thread id and thread name */
544545
private void setDefaultSpanData(final @NotNull ISpan span) {
545546
final @NotNull IThreadChecker threadChecker = scopes.getOptions().getThreadChecker();
546-
final @NotNull SentryId profilerId =
547-
scopes.getOptions().getContinuousProfiler().getProfilerId();
547+
final @NotNull SentryId profilerId = getProfilerId();
548548
if (!profilerId.equals(SentryId.EMPTY_ID) && Boolean.TRUE.equals(span.isSampled())) {
549549
span.setData(SpanDataConvention.PROFILER_ID, profilerId.toString());
550550
}
@@ -553,6 +553,12 @@ private void setDefaultSpanData(final @NotNull ISpan span) {
553553
span.setData(SpanDataConvention.THREAD_NAME, threadChecker.getCurrentThreadName());
554554
}
555555

556+
private @NotNull SentryId getProfilerId() {
557+
return !root.getSpanContext().getProfilerId().equals(SentryId.EMPTY_ID)
558+
? root.getSpanContext().getProfilerId()
559+
: scopes.getOptions().getContinuousProfiler().getProfilerId();
560+
}
561+
556562
@Override
557563
public @NotNull ISpan startChild(final @NotNull String operation) {
558564
return this.startChild(operation, (String) null);

sentry/src/main/java/io/sentry/SpanContext.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ public class SpanContext implements JsonUnknown, JsonSerializable {
5555

5656
protected @Nullable Baggage baggage;
5757

58+
/**
59+
* Set the profiler id associated with this transaction. If set to a non-empty id, this value will
60+
* be sent to sentry instead of {@link SentryOptions#getContinuousProfiler}
61+
*/
62+
private @NotNull SentryId profilerId = SentryId.EMPTY_ID;
63+
5864
public SpanContext(
5965
final @NotNull String operation, final @Nullable TracesSamplingDecision samplingDecision) {
6066
this(new SentryId(), new SpanId(), operation, null, samplingDecision);
@@ -304,6 +310,14 @@ public int hashCode() {
304310
return Objects.hash(traceId, spanId, parentSpanId, op, description, getStatus());
305311
}
306312

313+
public @NotNull SentryId getProfilerId() {
314+
return profilerId;
315+
}
316+
317+
public void setProfilerId(@NotNull SentryId profilerId) {
318+
this.profilerId = profilerId;
319+
}
320+
307321
// region JsonSerializable
308322

309323
public static final class JsonKeys {

0 commit comments

Comments
 (0)