Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a4ce01a
also load converter at sentry initialization time and hold in sentry-…
lbloder Oct 17, 2025
39225ff
use InitUtil to initialize profiler and converter
lbloder Oct 20, 2025
40395fb
add new configuration class to spring and spring4 variants
lbloder Oct 21, 2025
f8f253f
add tests for profiler and converter init
lbloder Oct 23, 2025
c02c3dd
format code
lbloder Oct 23, 2025
d180c19
add test for auto profiler config
lbloder Oct 24, 2025
5e75584
bump api
lbloder Oct 24, 2025
dde02b9
Merge branch 'main' into feat/profiling-w-spring-otel-agent
lbloder Nov 3, 2025
0514903
add missing package to test, adapt log statement
lbloder Nov 3, 2025
2cb26e1
add changelog entry, improve logs
lbloder Nov 3, 2025
737c147
Merge branch 'main' into feat/profiling-w-spring-otel-agent
lbloder Nov 3, 2025
41d2c6a
Update sentry-spring-7/src/main/java/io/sentry/spring7/SentryProfiler…
lbloder Nov 4, 2025
87071dd
Format code
getsentry-bot Nov 4, 2025
8fca29a
make initUtil methods more readable, return value from options, impro…
lbloder Nov 4, 2025
40bb0a2
Merge branch 'feat/profiling-w-spring-otel-agent' of github.com:getse…
lbloder Nov 4, 2025
4a52a0d
add tests for path creation
lbloder Nov 5, 2025
f04bdbc
improve test cases
lbloder Nov 7, 2025
c3c36b6
add condition on JavaContinuousProfiler to be on the classpath to inv…
lbloder Nov 10, 2025
3ebaeb5
Merge branch 'main' into feat/profiling-w-spring-otel-agent
lbloder Nov 10, 2025
fa864ed
Merge branch 'main' into feat/profiling-w-spring-otel-agent
lbloder Nov 10, 2025
1bb7e19
Merge branch 'main' into feat/profiling-w-spring-otel-agent
lbloder Nov 10, 2025
adf084c
return profiler and converter from options instead of a instance of noop
lbloder Nov 10, 2025
23ebbf7
Merge branch 'main' into feat/profiling-w-spring-otel-agent
lbloder Nov 11, 2025
937782e
Merge branch 'main' into feat/profiling-w-spring-otel-agent
lbloder Nov 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import io.sentry.profiling.JavaProfileConverterProvider;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
* AsyncProfiler implementation of {@link JavaProfileConverterProvider}. This provider integrates
Expand All @@ -15,7 +14,7 @@
public final class AsyncProfilerProfileConverterProvider implements JavaProfileConverterProvider {

@Override
public @Nullable IProfileConverter getProfileConverter() {
public @NotNull IProfileConverter getProfileConverter() {
return new AsyncProfilerProfileConverter();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ public class io/sentry/spring/boot/jakarta/SentryLogbackInitializer : org/spring
public fun supportsEventType (Lorg/springframework/core/ResolvableType;)Z
}

public class io/sentry/spring/boot/jakarta/SentryProfilerAutoConfiguration {
public fun <init> ()V
}

public class io/sentry/spring/boot/jakarta/SentryProperties : io/sentry/SentryOptions {
public fun <init> ()V
public fun getExceptionResolverOrder ()I
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.sentry.spring.boot.jakarta;

import com.jakewharton.nopen.annotation.Open;
import io.sentry.spring.jakarta.SentryProfilerConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(name = {"io.sentry.opentelemetry.agent.AgentMarker"})
Copy link
Collaborator Author

@lbloder lbloder Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make it conditional on the async profiler class too?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, I see no reason to run this if profiler isn't there.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What might become an issue is, if we ever have a second IContinuousProfiler implementation, we also have to update the condition

@Open
@Import(SentryProfilerConfiguration.class)
public class SentryProfilerAutoConfiguration {}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
io.sentry.spring.boot.jakarta.SentryAutoConfiguration
io.sentry.spring.boot.jakarta.SentryProfilerAutoConfiguration
io.sentry.spring.boot.jakarta.SentryLogbackAppenderAutoConfiguration
io.sentry.spring.boot.jakarta.SentryWebfluxAutoConfiguration
6 changes: 6 additions & 0 deletions sentry-spring-jakarta/api/sentry-spring-jakarta.api
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ public class io/sentry/spring/jakarta/SentryInitBeanPostProcessor : org/springfr
public fun setApplicationContext (Lorg/springframework/context/ApplicationContext;)V
}

public class io/sentry/spring/jakarta/SentryProfilerConfiguration {
public fun <init> ()V
public fun sentryOpenTelemetryProfilerConfiguration ()Lio/sentry/IContinuousProfiler;
public fun sentryOpenTelemetryProfilerConverterConfiguration ()Lio/sentry/IProfileConverter;
}

public class io/sentry/spring/jakarta/SentryRequestHttpServletRequestProcessor : io/sentry/EventProcessor {
public fun <init> (Lio/sentry/spring/jakarta/tracing/TransactionNameProvider;Ljakarta/servlet/http/HttpServletRequest;)V
public fun getOrder ()Ljava/lang/Long;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package io.sentry.spring.jakarta;

import com.jakewharton.nopen.annotation.Open;
import io.sentry.IContinuousProfiler;
import io.sentry.IProfileConverter;
import io.sentry.NoOpContinuousProfiler;
import io.sentry.NoOpProfileConverter;
import io.sentry.Sentry;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.profiling.ProfilingServiceLoader;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* Handles late initialization of the profiler if the application is run with the OTEL Agent in
* auto-init mode. In that case the agent cannot initialize the profiler yet and falls back to No-Op
* implementations. This Configuration sets the profiler and converter on the options if that was
* the case.
*/
@Configuration(proxyBeanMethods = false)
@Open
public class SentryProfilerConfiguration {

@Bean
@ConditionalOnMissingBean(name = "sentryOpenTelemetryProfilerConfiguration")
public IContinuousProfiler sentryOpenTelemetryProfilerConfiguration() {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extract init into util to streamline initialization code

SentryOptions options = Sentry.getGlobalScope().getOptions();
IContinuousProfiler profiler = NoOpContinuousProfiler.getInstance();

if (Sentry.isEnabled()
&& options.isContinuousProfilingEnabled()
&& options.getContinuousProfiler() instanceof NoOpContinuousProfiler) {

options
.getLogger()
.log(
SentryLevel.DEBUG,
"Continuous profiler is NoOp, attempting to reload with Spring Boot classloader");

String path = options.getProfilingTracesDirPath();

profiler =
ProfilingServiceLoader.loadContinuousProfiler(
options.getLogger(),
path != null ? path : "",
options.getProfilingTracesHz(),
options.getExecutorService());

options.setContinuousProfiler(profiler);

if (!(profiler instanceof NoOpContinuousProfiler)) {
options
.getLogger()
.log(
SentryLevel.INFO,
"Successfully loaded continuous profiler via Spring Boot classloader");
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Sentry profiler: Custom configuration discarded.

When Sentry is disabled, the method returns a new NoOpContinuousProfiler instance instead of the existing profiler from options. This is inconsistent with sentry-spring/SentryProfilerConfiguration.java which returns options.getContinuousProfiler(). If options already has a configured profiler (e.g., set via custom configuration), returning a NoOp instance would discard it and potentially break profiling functionality. The unused local variable profiler suggests this was meant to be options.getContinuousProfiler().

Fix in Cursor Fix in Web

return profiler;
}

@Bean
@ConditionalOnMissingBean(name = "sentryOpenTelemetryProfilerConverterConfiguration")
public IProfileConverter sentryOpenTelemetryProfilerConverterConfiguration() {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extract init into util to streamline initialization code

SentryOptions options = Sentry.getGlobalScope().getOptions();
IProfileConverter converter = NoOpProfileConverter.getInstance();

if (Sentry.isEnabled()
&& options.isContinuousProfilingEnabled()
&& options.getProfilerConverter() instanceof NoOpProfileConverter) {

options
.getLogger()
.log(
SentryLevel.DEBUG,
"Profile converter is NoOp, attempting to reload with Spring Boot classloader");

converter = ProfilingServiceLoader.loadProfileConverter();

options.setProfilerConverter(converter);

if (!(converter instanceof NoOpProfileConverter)) {
options
.getLogger()
.log(
SentryLevel.INFO,
"Successfully loaded profile converter via Spring Boot classloader");
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Sentry Disabled: Configured Converter Ignored

When Sentry is disabled, the method returns a new NoOpProfileConverter instance instead of the existing converter from options. This is inconsistent with sentry-spring/SentryProfilerConfiguration.java which returns options.getProfilerConverter(). If options already has a configured converter, returning a NoOp instance would discard it and potentially break profile conversion. The unused local variable converter suggests this was meant to be options.getProfilerConverter().

Fix in Cursor Fix in Web

return converter;
}
}
8 changes: 8 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -1582,6 +1582,11 @@ public final class io/sentry/NoOpLogger : io/sentry/ILogger {
public fun log (Lio/sentry/SentryLevel;Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
}

public final class io/sentry/NoOpProfileConverter : io/sentry/IProfileConverter {
public fun convertFromFile (Ljava/lang/String;)Lio/sentry/protocol/profiling/SentryProfile;
public static fun getInstance ()Lio/sentry/NoOpProfileConverter;
}

public final class io/sentry/NoOpReplayBreadcrumbConverter : io/sentry/ReplayBreadcrumbConverter {
public fun convert (Lio/sentry/Breadcrumb;)Lio/sentry/rrweb/RRWebEvent;
public static fun getInstance ()Lio/sentry/NoOpReplayBreadcrumbConverter;
Expand Down Expand Up @@ -2893,6 +2898,7 @@ public final class io/sentry/SentryEnvelopeItem {
public static fun fromEvent (Lio/sentry/ISerializer;Lio/sentry/SentryBaseEvent;)Lio/sentry/SentryEnvelopeItem;
public static fun fromLogs (Lio/sentry/ISerializer;Lio/sentry/SentryLogEvents;)Lio/sentry/SentryEnvelopeItem;
public static fun fromProfileChunk (Lio/sentry/ProfileChunk;Lio/sentry/ISerializer;)Lio/sentry/SentryEnvelopeItem;
public static fun fromProfileChunk (Lio/sentry/ProfileChunk;Lio/sentry/ISerializer;Lio/sentry/IProfileConverter;)Lio/sentry/SentryEnvelopeItem;
public static fun fromProfilingTrace (Lio/sentry/ProfilingTraceData;JLio/sentry/ISerializer;)Lio/sentry/SentryEnvelopeItem;
public static fun fromReplay (Lio/sentry/ISerializer;Lio/sentry/ILogger;Lio/sentry/SentryReplayEvent;Lio/sentry/ReplayRecording;Z)Lio/sentry/SentryEnvelopeItem;
public static fun fromSession (Lio/sentry/ISerializer;Lio/sentry/Session;)Lio/sentry/SentryEnvelopeItem;
Expand Down Expand Up @@ -3385,6 +3391,7 @@ public class io/sentry/SentryOptions {
public fun getPerformanceCollectors ()Ljava/util/List;
public fun getProfileLifecycle ()Lio/sentry/ProfileLifecycle;
public fun getProfileSessionSampleRate ()Ljava/lang/Double;
public fun getProfilerConverter ()Lio/sentry/IProfileConverter;
public fun getProfilesSampleRate ()Ljava/lang/Double;
public fun getProfilesSampler ()Lio/sentry/SentryOptions$ProfilesSamplerCallback;
public fun getProfilingTracesDirPath ()Ljava/lang/String;
Expand Down Expand Up @@ -3530,6 +3537,7 @@ public class io/sentry/SentryOptions {
public fun setPrintUncaughtStackTrace (Z)V
public fun setProfileLifecycle (Lio/sentry/ProfileLifecycle;)V
public fun setProfileSessionSampleRate (Ljava/lang/Double;)V
public fun setProfilerConverter (Lio/sentry/IProfileConverter;)V
public fun setProfilesSampleRate (Ljava/lang/Double;)V
public fun setProfilesSampler (Lio/sentry/SentryOptions$ProfilesSamplerCallback;)V
public fun setProfilingTracesDirPath (Ljava/lang/String;)V
Expand Down
21 changes: 21 additions & 0 deletions sentry/src/main/java/io/sentry/NoOpProfileConverter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.sentry;

import io.sentry.protocol.profiling.SentryProfile;
import java.io.IOException;
import org.jetbrains.annotations.NotNull;

public final class NoOpProfileConverter implements IProfileConverter {

private static final NoOpProfileConverter instance = new NoOpProfileConverter();

private NoOpProfileConverter() {}

public static NoOpProfileConverter getInstance() {
return instance;
}

@Override
public @NotNull SentryProfile convertFromFile(@NotNull String jfrFilePath) throws IOException {
return new SentryProfile();
}
}
5 changes: 5 additions & 0 deletions sentry/src/main/java/io/sentry/Sentry.java
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,11 @@ private static void initJvmContinuousProfiling(@NotNull SentryOptions options) {
options.getExecutorService());

options.setContinuousProfiler(continuousProfiler);

final IProfileConverter profileConverter = ProfilingServiceLoader.loadProfileConverter();
if (profileConverter != null) {
options.setProfilerConverter(profileConverter);
}
} catch (Exception e) {
options
.getLogger()
Expand Down
3 changes: 2 additions & 1 deletion sentry/src/main/java/io/sentry/SentryClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -983,7 +983,8 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint
new SentryEnvelope(
new SentryEnvelopeHeader(sentryId, options.getSdkVersion(), null),
Collections.singletonList(
SentryEnvelopeItem.fromProfileChunk(profileChunk, options.getSerializer())));
SentryEnvelopeItem.fromProfileChunk(
profileChunk, options.getSerializer(), options.getProfilerConverter())));
sentryId = sendEnvelope(envelope, null);
} catch (IOException | SentryEnvelopeException e) {
options
Expand Down
14 changes: 10 additions & 4 deletions sentry/src/main/java/io/sentry/SentryEnvelopeItem.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import io.sentry.clientreport.ClientReport;
import io.sentry.exception.SentryEnvelopeException;
import io.sentry.profiling.ProfilingServiceLoader;
import io.sentry.protocol.SentryTransaction;
import io.sentry.protocol.profiling.SentryProfile;
import io.sentry.util.FileUtils;
Expand Down Expand Up @@ -283,6 +282,15 @@ private static void ensureAttachmentSizeLimit(
final @NotNull ProfileChunk profileChunk, final @NotNull ISerializer serializer)
throws SentryEnvelopeException {

return fromProfileChunk(profileChunk, serializer, NoOpProfileConverter.getInstance());
}

public static @NotNull SentryEnvelopeItem fromProfileChunk(
final @NotNull ProfileChunk profileChunk,
final @NotNull ISerializer serializer,
final @NotNull IProfileConverter profileConverter)
throws SentryEnvelopeException {

final @NotNull File traceFile = profileChunk.getTraceFile();
// Using CachedItem, so we read the trace file in the background
final CachedItem cachedItem =
Expand All @@ -296,9 +304,7 @@ private static void ensureAttachmentSizeLimit(
}

if (ProfileChunk.PLATFORM_JAVA.equals(profileChunk.getPlatform())) {
final IProfileConverter profileConverter =
ProfilingServiceLoader.loadProfileConverter();
if (profileConverter != null) {
if (!NoOpProfileConverter.getInstance().equals(profileConverter)) {
try {
final SentryProfile profile =
profileConverter.convertFromFile(traceFile.getAbsolutePath());
Expand Down
11 changes: 11 additions & 0 deletions sentry/src/main/java/io/sentry/SentryOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,9 @@ public class SentryOptions {
/** Profiler that runs continuously until stopped. */
private @NotNull IContinuousProfiler continuousProfiler = NoOpContinuousProfiler.getInstance();

/** Profiler that runs continuously until stopped. */
private @NotNull IProfileConverter profilerConverter = NoOpProfileConverter.getInstance();

/**
* Contains a list of origins to which `sentry-trace` header should be sent in HTTP integrations.
*/
Expand Down Expand Up @@ -604,6 +607,14 @@ public class SentryOptions {

private @Nullable String profilingTracesDirPath;

public @NotNull IProfileConverter getProfilerConverter() {
return profilerConverter;
}

public void setProfilerConverter(@NotNull IProfileConverter profilerConverter) {
this.profilerConverter = profilerConverter;
}

/**
* Configuration options for Sentry Build Distribution. NOTE: Ideally this would be in
* SentryAndroidOptions, but there's a circular dependency issue between sentry-android-core and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import io.sentry.IProfileConverter;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.NotNull;

/**
* Service provider interface for creating profile converters.
Expand All @@ -18,6 +18,6 @@ public interface JavaProfileConverterProvider {
*
* @return a profile converter instance, or null if the provider cannot create one
*/
@Nullable
@NotNull
IProfileConverter getProfileConverter();
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import io.sentry.IProfileConverter;
import io.sentry.ISentryExecutorService;
import io.sentry.NoOpContinuousProfiler;
import io.sentry.NoOpProfileConverter;
import io.sentry.ScopesAdapter;
import io.sentry.SentryLevel;
import java.util.Iterator;
Expand Down Expand Up @@ -51,7 +52,7 @@ public final class ProfilingServiceLoader {
*
* @return an IProfileConverter instance or null if no provider is found
*/
public static @Nullable IProfileConverter loadProfileConverter() {
public static @NotNull IProfileConverter loadProfileConverter() {
ILogger logger = ScopesAdapter.getInstance().getGlobalScope().getOptions().getLogger();
try {
JavaProfileConverterProvider provider =
Expand All @@ -63,12 +64,16 @@ public final class ProfilingServiceLoader {
provider.getClass().getName());
return provider.getProfileConverter();
} else {
logger.log(SentryLevel.DEBUG, "No profile converter provider found, returning null");
return null;
logger.log(
SentryLevel.DEBUG, "No profile converter provider found, using NoOpProfileConverter");
return NoOpProfileConverter.getInstance();
}
} catch (Throwable t) {
logger.log(SentryLevel.ERROR, "Failed to load profile converter provider, returning null", t);
return null;
logger.log(
SentryLevel.ERROR,
"Failed to load profile converter provider, using NoOpProfileConverter",
t);
return NoOpProfileConverter.getInstance();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class ProfilingServiceLoaderTest {
}

class JavaProfileConverterProviderStub : JavaProfileConverterProvider {
override fun getProfileConverter(): IProfileConverter? {
override fun getProfileConverter(): IProfileConverter {
return ProfileConverterStub()
}
}
Expand Down
Loading