From a4ce01a999983249ce0ca78ab88eb725e6984438 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Fri, 17 Oct 2025 15:04:12 +0200 Subject: [PATCH 01/13] also load converter at sentry initialization time and hold in sentry-options, add configuration class to load the profiler and converter after spring boot starts in agent mode --- ...AsyncProfilerProfileConverterProvider.java | 3 +- .../api/sentry-spring-boot-jakarta.api | 4 + .../SentryProfilerAutoConfiguration.java | 13 +++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../api/sentry-spring-jakarta.api | 6 ++ .../jakarta/SentryProfilerConfiguration.java | 94 +++++++++++++++++++ sentry/api/sentry.api | 8 ++ .../java/io/sentry/NoOpProfileConverter.java | 21 +++++ sentry/src/main/java/io/sentry/Sentry.java | 5 + .../src/main/java/io/sentry/SentryClient.java | 3 +- .../java/io/sentry/SentryEnvelopeItem.java | 14 ++- .../main/java/io/sentry/SentryOptions.java | 11 +++ .../JavaProfileConverterProvider.java | 4 +- .../profiling/ProfilingServiceLoader.java | 15 ++- .../profiling/ProfilingServiceLoaderTest.kt | 2 +- 15 files changed, 189 insertions(+), 15 deletions(-) create mode 100644 sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryProfilerAutoConfiguration.java create mode 100644 sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryProfilerConfiguration.java create mode 100644 sentry/src/main/java/io/sentry/NoOpProfileConverter.java diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider.java index b8aa9111fa..a106cffc7d 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider.java @@ -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 @@ -15,7 +14,7 @@ public final class AsyncProfilerProfileConverterProvider implements JavaProfileConverterProvider { @Override - public @Nullable IProfileConverter getProfileConverter() { + public @NotNull IProfileConverter getProfileConverter() { return new AsyncProfilerProfileConverter(); } diff --git a/sentry-spring-boot-jakarta/api/sentry-spring-boot-jakarta.api b/sentry-spring-boot-jakarta/api/sentry-spring-boot-jakarta.api index b0ef970d7d..197bdbeef7 100644 --- a/sentry-spring-boot-jakarta/api/sentry-spring-boot-jakarta.api +++ b/sentry-spring-boot-jakarta/api/sentry-spring-boot-jakarta.api @@ -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 ()V +} + public class io/sentry/spring/boot/jakarta/SentryProperties : io/sentry/SentryOptions { public fun ()V public fun getExceptionResolverOrder ()I diff --git a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryProfilerAutoConfiguration.java b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryProfilerAutoConfiguration.java new file mode 100644 index 0000000000..8108e2cc02 --- /dev/null +++ b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryProfilerAutoConfiguration.java @@ -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"}) +@Open +@Import(SentryProfilerConfiguration.class) +public class SentryProfilerAutoConfiguration {} diff --git a/sentry-spring-boot-jakarta/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sentry-spring-boot-jakarta/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 41436fe883..1a81267001 100644 --- a/sentry-spring-boot-jakarta/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/sentry-spring-boot-jakarta/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -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 diff --git a/sentry-spring-jakarta/api/sentry-spring-jakarta.api b/sentry-spring-jakarta/api/sentry-spring-jakarta.api index 3c1db200cb..f28f4153b5 100644 --- a/sentry-spring-jakarta/api/sentry-spring-jakarta.api +++ b/sentry-spring-jakarta/api/sentry-spring-jakarta.api @@ -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 ()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 (Lio/sentry/spring/jakarta/tracing/TransactionNameProvider;Ljakarta/servlet/http/HttpServletRequest;)V public fun getOrder ()Ljava/lang/Long; diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryProfilerConfiguration.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryProfilerConfiguration.java new file mode 100644 index 0000000000..df9679a043 --- /dev/null +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryProfilerConfiguration.java @@ -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() { + 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"); + } + } + return profiler; + } + + @Bean + @ConditionalOnMissingBean(name = "sentryOpenTelemetryProfilerConverterConfiguration") + public IProfileConverter sentryOpenTelemetryProfilerConverterConfiguration() { + 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"); + } + } + return converter; + } +} diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 3964619e0b..c8036f7ac8 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -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; @@ -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; @@ -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; @@ -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 diff --git a/sentry/src/main/java/io/sentry/NoOpProfileConverter.java b/sentry/src/main/java/io/sentry/NoOpProfileConverter.java new file mode 100644 index 0000000000..5a733a6697 --- /dev/null +++ b/sentry/src/main/java/io/sentry/NoOpProfileConverter.java @@ -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(); + } +} diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index a28ee936e6..a17359e8fa 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -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() diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index bfcf4e780b..f4127e86ec 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -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 diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index c3b77679ec..0dbc356161 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -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; @@ -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 = @@ -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()); diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 3a81b268b2..79af3df662 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -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. */ @@ -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 diff --git a/sentry/src/main/java/io/sentry/profiling/JavaProfileConverterProvider.java b/sentry/src/main/java/io/sentry/profiling/JavaProfileConverterProvider.java index e1fcdfa879..69488f6021 100644 --- a/sentry/src/main/java/io/sentry/profiling/JavaProfileConverterProvider.java +++ b/sentry/src/main/java/io/sentry/profiling/JavaProfileConverterProvider.java @@ -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. @@ -18,6 +18,6 @@ public interface JavaProfileConverterProvider { * * @return a profile converter instance, or null if the provider cannot create one */ - @Nullable + @NotNull IProfileConverter getProfileConverter(); } diff --git a/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java b/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java index c09a8ea301..cf86a13fc5 100644 --- a/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java +++ b/sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java @@ -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; @@ -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 = @@ -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(); } } diff --git a/sentry/src/test/java/io/sentry/profiling/ProfilingServiceLoaderTest.kt b/sentry/src/test/java/io/sentry/profiling/ProfilingServiceLoaderTest.kt index ddbae8c8f9..0fed85995d 100644 --- a/sentry/src/test/java/io/sentry/profiling/ProfilingServiceLoaderTest.kt +++ b/sentry/src/test/java/io/sentry/profiling/ProfilingServiceLoaderTest.kt @@ -28,7 +28,7 @@ class ProfilingServiceLoaderTest { } class JavaProfileConverterProviderStub : JavaProfileConverterProvider { - override fun getProfileConverter(): IProfileConverter? { + override fun getProfileConverter(): IProfileConverter { return ProfileConverterStub() } } From 39225ff54898d5bacbf206211cefe966b7632d8d Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Mon, 20 Oct 2025 15:24:12 +0200 Subject: [PATCH 02/13] use InitUtil to initialize profiler and converter --- .../jakarta/SentryProfilerConfiguration.java | 62 +++------------ sentry/api/sentry.api | 2 + sentry/src/main/java/io/sentry/Sentry.java | 40 +--------- .../main/java/io/sentry/util/InitUtil.java | 76 +++++++++++++++++++ 4 files changed, 89 insertions(+), 91 deletions(-) diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryProfilerConfiguration.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryProfilerConfiguration.java index df9679a043..9faf15466d 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryProfilerConfiguration.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryProfilerConfiguration.java @@ -6,9 +6,8 @@ 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 io.sentry.util.InitUtil; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -29,36 +28,11 @@ public IContinuousProfiler sentryOpenTelemetryProfilerConfiguration() { 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"); - } + if (Sentry.isEnabled()) { + return InitUtil.initializeProfiler(options); + } else { + return profiler; } - return profiler; } @Bean @@ -67,28 +41,10 @@ public IProfileConverter sentryOpenTelemetryProfilerConverterConfiguration() { 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"); - } + if (Sentry.isEnabled()) { + return InitUtil.initializeProfileConverter(options); + } else { + return converter; } - return converter; } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index c8036f7ac8..802aa54089 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -7052,6 +7052,8 @@ public final class io/sentry/util/HttpUtils { public final class io/sentry/util/InitUtil { public fun ()V + public static fun initializeProfileConverter (Lio/sentry/SentryOptions;)Lio/sentry/IProfileConverter; + public static fun initializeProfiler (Lio/sentry/SentryOptions;)Lio/sentry/IContinuousProfiler; public static fun shouldInit (Lio/sentry/SentryOptions;Lio/sentry/SentryOptions;Z)Z } diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index a17359e8fa..73231b9f5c 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -15,7 +15,6 @@ import io.sentry.internal.modules.ResourcesModulesLoader; import io.sentry.logger.ILoggerApi; import io.sentry.opentelemetry.OpenTelemetryUtil; -import io.sentry.profiling.ProfilingServiceLoader; import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.User; @@ -691,43 +690,8 @@ private static void initConfigurations(final @NotNull SentryOptions options) { } private static void initJvmContinuousProfiling(@NotNull SentryOptions options) { - - if (options.isContinuousProfilingEnabled() - && options.getContinuousProfiler() == NoOpContinuousProfiler.getInstance()) { - try { - String profilingTracesDirPath = options.getProfilingTracesDirPath(); - if (profilingTracesDirPath == null) { - File tempDir = new File(System.getProperty("java.io.tmpdir"), "sentry_profiling_traces"); - boolean createDirectorySuccess = tempDir.mkdirs() || tempDir.exists(); - - if (!createDirectorySuccess) { - throw new IllegalArgumentException( - "Creating a fallback directory for profiling failed in " - + tempDir.getAbsolutePath()); - } - profilingTracesDirPath = tempDir.getAbsolutePath(); - options.setProfilingTracesDirPath(profilingTracesDirPath); - } - - final IContinuousProfiler continuousProfiler = - ProfilingServiceLoader.loadContinuousProfiler( - options.getLogger(), - profilingTracesDirPath, - options.getProfilingTracesHz(), - options.getExecutorService()); - - options.setContinuousProfiler(continuousProfiler); - - final IProfileConverter profileConverter = ProfilingServiceLoader.loadProfileConverter(); - if (profileConverter != null) { - options.setProfilerConverter(profileConverter); - } - } catch (Exception e) { - options - .getLogger() - .log(SentryLevel.ERROR, "Failed to create default profiling traces directory", e); - } - } + InitUtil.initializeProfiler(options); + InitUtil.initializeProfileConverter(options); } /** Close the SDK */ diff --git a/sentry/src/main/java/io/sentry/util/InitUtil.java b/sentry/src/main/java/io/sentry/util/InitUtil.java index b598f51d86..676b160a80 100644 --- a/sentry/src/main/java/io/sentry/util/InitUtil.java +++ b/sentry/src/main/java/io/sentry/util/InitUtil.java @@ -1,9 +1,15 @@ package io.sentry.util; +import io.sentry.IContinuousProfiler; +import io.sentry.IProfileConverter; import io.sentry.ManifestVersionDetector; +import io.sentry.NoOpContinuousProfiler; +import io.sentry.NoOpProfileConverter; import io.sentry.NoopVersionDetector; import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import io.sentry.profiling.ProfilingServiceLoader; +import java.io.File; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -46,4 +52,74 @@ public static boolean shouldInit( return previousOptions.getInitPriority().ordinal() <= newOptions.getInitPriority().ordinal(); } + + public static IContinuousProfiler initializeProfiler(@NotNull SentryOptions options) { + IContinuousProfiler continuousProfiler = NoOpContinuousProfiler.getInstance(); + + if (options.isContinuousProfilingEnabled() + && options.getContinuousProfiler() == NoOpContinuousProfiler.getInstance()) { + try { + String profilingTracesDirPath = options.getProfilingTracesDirPath(); + if (profilingTracesDirPath == null) { + File tempDir = new File(System.getProperty("java.io.tmpdir"), "sentry_profiling_traces"); + boolean createDirectorySuccess = tempDir.mkdirs() || tempDir.exists(); + + if (!createDirectorySuccess) { + throw new IllegalArgumentException( + "Creating a fallback directory for profiling failed in " + + tempDir.getAbsolutePath()); + } + profilingTracesDirPath = tempDir.getAbsolutePath(); + options.setProfilingTracesDirPath(profilingTracesDirPath); + } + + continuousProfiler = + ProfilingServiceLoader.loadContinuousProfiler( + options.getLogger(), + profilingTracesDirPath, + options.getProfilingTracesHz(), + options.getExecutorService()); + + if (!(continuousProfiler instanceof NoOpContinuousProfiler)) { + options.setContinuousProfiler(continuousProfiler); + } + + return continuousProfiler; + + } catch (Exception e) { + options + .getLogger() + .log(SentryLevel.ERROR, "Failed to create default profiling traces directory", e); + } + } + return continuousProfiler; + } + + public static IProfileConverter initializeProfileConverter(@NotNull SentryOptions options) { + IProfileConverter converter = NoOpProfileConverter.getInstance(); + + if (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"); + } + } + return converter; + } + // TODO: Add initialization of profiler here } From 40395fb046a79d6fd4c84085a44ad7eef5ff8722 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 21 Oct 2025 16:28:34 +0200 Subject: [PATCH 03/13] add new configuration class to spring and spring4 variants --- .../spring7/SentryProfilerConfiguration.java | 50 +++++++++++++++++++ .../SentryProfilerAutoConfiguration.java | 13 +++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../boot/SentryProfilerAutoConfiguration.java | 13 +++++ .../main/resources/META-INF/spring.factories | 1 + .../spring/SentryProfilerConfiguration.java | 50 +++++++++++++++++++ 6 files changed, 128 insertions(+) create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring7/SentryProfilerConfiguration.java create mode 100644 sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryProfilerAutoConfiguration.java create mode 100644 sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryProfilerAutoConfiguration.java create mode 100644 sentry-spring/src/main/java/io/sentry/spring/SentryProfilerConfiguration.java diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/SentryProfilerConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryProfilerConfiguration.java new file mode 100644 index 0000000000..939d5df98d --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryProfilerConfiguration.java @@ -0,0 +1,50 @@ +package io.sentry.spring7; + +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.SentryOptions; +import io.sentry.util.InitUtil; +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() { + SentryOptions options = Sentry.getGlobalScope().getOptions(); + IContinuousProfiler profiler = NoOpContinuousProfiler.getInstance(); + + if (Sentry.isEnabled()) { + return InitUtil.initializeProfiler(options); + } else { + return profiler; + } + } + + @Bean + @ConditionalOnMissingBean(name = "sentryOpenTelemetryProfilerConverterConfiguration") + public IProfileConverter sentryOpenTelemetryProfilerConverterConfiguration() { + SentryOptions options = Sentry.getGlobalScope().getOptions(); + IProfileConverter converter = NoOpProfileConverter.getInstance(); + + if (Sentry.isEnabled()) { + return InitUtil.initializeProfileConverter(options); + } else { + return converter; + } + } +} diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryProfilerAutoConfiguration.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryProfilerAutoConfiguration.java new file mode 100644 index 0000000000..e8338c7e82 --- /dev/null +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryProfilerAutoConfiguration.java @@ -0,0 +1,13 @@ +package io.sentry.spring.boot4; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.spring7.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"}) +@Open +@Import(SentryProfilerConfiguration.class) +public class SentryProfilerAutoConfiguration {} diff --git a/sentry-spring-boot-4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sentry-spring-boot-4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 4697c6b6a9..a108fa2ca1 100644 --- a/sentry-spring-boot-4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/sentry-spring-boot-4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,3 +1,4 @@ io.sentry.spring.boot4.SentryAutoConfiguration +io.sentry.spring.boot4.SentryProfilerAutoConfiguration io.sentry.spring.boot4.SentryLogbackAppenderAutoConfiguration io.sentry.spring.boot4.SentryWebfluxAutoConfiguration diff --git a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryProfilerAutoConfiguration.java b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryProfilerAutoConfiguration.java new file mode 100644 index 0000000000..952f842165 --- /dev/null +++ b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryProfilerAutoConfiguration.java @@ -0,0 +1,13 @@ +package io.sentry.spring.boot; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.spring.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"}) +@Open +@Import(SentryProfilerConfiguration.class) +public class SentryProfilerAutoConfiguration {} diff --git a/sentry-spring-boot/src/main/resources/META-INF/spring.factories b/sentry-spring-boot/src/main/resources/META-INF/spring.factories index 9712f4b407..5b27df50bb 100644 --- a/sentry-spring-boot/src/main/resources/META-INF/spring.factories +++ b/sentry-spring-boot/src/main/resources/META-INF/spring.factories @@ -1,5 +1,6 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ io.sentry.spring.boot.SentryAutoConfiguration,\ +io.sentry.spring.boot.SentryProfilerAutoConfiguration,\ io.sentry.spring.boot.SentryLogbackAppenderAutoConfiguration,\ io.sentry.spring.boot.SentryWebfluxAutoConfiguration diff --git a/sentry-spring/src/main/java/io/sentry/spring/SentryProfilerConfiguration.java b/sentry-spring/src/main/java/io/sentry/spring/SentryProfilerConfiguration.java new file mode 100644 index 0000000000..9bb7b713b5 --- /dev/null +++ b/sentry-spring/src/main/java/io/sentry/spring/SentryProfilerConfiguration.java @@ -0,0 +1,50 @@ +package io.sentry.spring; + +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.SentryOptions; +import io.sentry.util.InitUtil; +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() { + SentryOptions options = Sentry.getGlobalScope().getOptions(); + IContinuousProfiler profiler = NoOpContinuousProfiler.getInstance(); + + if (Sentry.isEnabled()) { + return InitUtil.initializeProfiler(options); + } else { + return profiler; + } + } + + @Bean + @ConditionalOnMissingBean(name = "sentryOpenTelemetryProfilerConverterConfiguration") + public IProfileConverter sentryOpenTelemetryProfilerConverterConfiguration() { + SentryOptions options = Sentry.getGlobalScope().getOptions(); + IProfileConverter converter = NoOpProfileConverter.getInstance(); + + if (Sentry.isEnabled()) { + return InitUtil.initializeProfileConverter(options); + } else { + return converter; + } + } +} From f8f253f82be4b8dfaec5733e098b57850ce524ab Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Thu, 23 Oct 2025 14:27:16 +0200 Subject: [PATCH 04/13] add tests for profiler and converter init --- .../init/AsyncProfilerInitUtilTest.kt | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/init/AsyncProfilerInitUtilTest.kt diff --git a/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/init/AsyncProfilerInitUtilTest.kt b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/init/AsyncProfilerInitUtilTest.kt new file mode 100644 index 0000000000..a11c7e40c5 --- /dev/null +++ b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/init/AsyncProfilerInitUtilTest.kt @@ -0,0 +1,76 @@ +import io.sentry.ILogger +import io.sentry.ISentryExecutorService +import io.sentry.NoOpContinuousProfiler +import io.sentry.NoOpProfileConverter +import io.sentry.SentryOptions +import io.sentry.asyncprofiler.profiling.JavaContinuousProfiler +import io.sentry.util.InitUtil +import kotlin.test.Test +import io.sentry.asyncprofiler.provider.AsyncProfilerProfileConverterProvider +import org.mockito.kotlin.mock + +class AsyncProfilerInitUtilTest { + + @Test + fun `initialize Profiler returns no-op profiler if profiling disabled`() { + val options = SentryOptions() + val profiler = InitUtil.initializeProfiler(options) + assert(profiler is NoOpContinuousProfiler) + } + + @Test + fun `initialize Converter returns no-op profiler if profiling disabled`() { + val options = SentryOptions() + val converter = InitUtil.initializeProfileConverter(options) + assert(converter is NoOpProfileConverter) + } + + @Test + fun `initialize Profiler returns no-op profiler if profiler already initialized`() { + val options = SentryOptions().also { + it.setProfileSessionSampleRate(1.0) + it.tracesSampleRate = 1.0 + it.setContinuousProfiler( + JavaContinuousProfiler( + mock(), "", 10, + mock() + ) + ) + } + + val profiler = InitUtil.initializeProfiler(options) + assert(profiler is NoOpContinuousProfiler) + } + + @Test + fun `initialize converter returns no-op converter if converter already initialized`() { + val options = SentryOptions().also { + it.setProfileSessionSampleRate(1.0) + it.tracesSampleRate = 1.0 + it.profilerConverter = AsyncProfilerProfileConverterProvider.AsyncProfilerProfileConverter() + } + + val converter = InitUtil.initializeProfileConverter(options) + assert(converter is NoOpProfileConverter) + } + + @Test + fun `initialize Profiler returns JavaContinuousProfiler if profiling enabled but profiler not yet initialized`() { + val options = SentryOptions().also { + it.setProfileSessionSampleRate(1.0) + it.tracesSampleRate = 1.0 + } + val profiler = InitUtil.initializeProfiler(options) + assert(profiler is JavaContinuousProfiler) + } + + @Test + fun `initialize Profiler returns AsyncProfilerProfileConverterProvider if profiling enabled but profiler not yet initialized`() { + val options = SentryOptions().also { + it.setProfileSessionSampleRate(1.0) + it.tracesSampleRate = 1.0 + } + val converter = InitUtil.initializeProfileConverter(options) + assert(converter is AsyncProfilerProfileConverterProvider.AsyncProfilerProfileConverter) + } +} From c02c3dd549e9a7fb955dbf1efff270333a5549c1 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Thu, 23 Oct 2025 14:45:46 +0200 Subject: [PATCH 05/13] format code --- .../init/AsyncProfilerInitUtilTest.kt | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/init/AsyncProfilerInitUtilTest.kt b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/init/AsyncProfilerInitUtilTest.kt index a11c7e40c5..e8bfd30117 100644 --- a/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/init/AsyncProfilerInitUtilTest.kt +++ b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/init/AsyncProfilerInitUtilTest.kt @@ -4,9 +4,9 @@ import io.sentry.NoOpContinuousProfiler import io.sentry.NoOpProfileConverter import io.sentry.SentryOptions import io.sentry.asyncprofiler.profiling.JavaContinuousProfiler +import io.sentry.asyncprofiler.provider.AsyncProfilerProfileConverterProvider import io.sentry.util.InitUtil import kotlin.test.Test -import io.sentry.asyncprofiler.provider.AsyncProfilerProfileConverterProvider import org.mockito.kotlin.mock class AsyncProfilerInitUtilTest { @@ -27,16 +27,14 @@ class AsyncProfilerInitUtilTest { @Test fun `initialize Profiler returns no-op profiler if profiler already initialized`() { - val options = SentryOptions().also { - it.setProfileSessionSampleRate(1.0) - it.tracesSampleRate = 1.0 - it.setContinuousProfiler( - JavaContinuousProfiler( - mock(), "", 10, - mock() + val options = + SentryOptions().also { + it.setProfileSessionSampleRate(1.0) + it.tracesSampleRate = 1.0 + it.setContinuousProfiler( + JavaContinuousProfiler(mock(), "", 10, mock()) ) - ) - } + } val profiler = InitUtil.initializeProfiler(options) assert(profiler is NoOpContinuousProfiler) @@ -44,11 +42,12 @@ class AsyncProfilerInitUtilTest { @Test fun `initialize converter returns no-op converter if converter already initialized`() { - val options = SentryOptions().also { - it.setProfileSessionSampleRate(1.0) - it.tracesSampleRate = 1.0 - it.profilerConverter = AsyncProfilerProfileConverterProvider.AsyncProfilerProfileConverter() - } + val options = + SentryOptions().also { + it.setProfileSessionSampleRate(1.0) + it.tracesSampleRate = 1.0 + it.profilerConverter = AsyncProfilerProfileConverterProvider.AsyncProfilerProfileConverter() + } val converter = InitUtil.initializeProfileConverter(options) assert(converter is NoOpProfileConverter) @@ -56,20 +55,22 @@ class AsyncProfilerInitUtilTest { @Test fun `initialize Profiler returns JavaContinuousProfiler if profiling enabled but profiler not yet initialized`() { - val options = SentryOptions().also { - it.setProfileSessionSampleRate(1.0) - it.tracesSampleRate = 1.0 - } + val options = + SentryOptions().also { + it.setProfileSessionSampleRate(1.0) + it.tracesSampleRate = 1.0 + } val profiler = InitUtil.initializeProfiler(options) assert(profiler is JavaContinuousProfiler) } @Test fun `initialize Profiler returns AsyncProfilerProfileConverterProvider if profiling enabled but profiler not yet initialized`() { - val options = SentryOptions().also { - it.setProfileSessionSampleRate(1.0) - it.tracesSampleRate = 1.0 - } + val options = + SentryOptions().also { + it.setProfileSessionSampleRate(1.0) + it.tracesSampleRate = 1.0 + } val converter = InitUtil.initializeProfileConverter(options) assert(converter is AsyncProfilerProfileConverterProvider.AsyncProfilerProfileConverter) } From d180c19ced9f956f57f9ab64748ac6b76cb5a124 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Fri, 24 Oct 2025 09:16:35 +0200 Subject: [PATCH 06/13] add test for auto profiler config --- .../boot4/SentryAutoConfigurationTest.kt | 36 +++++++++++++++++++ .../jakarta/SentryAutoConfigurationTest.kt | 36 +++++++++++++++++++ .../boot/SentryAutoConfigurationTest.kt | 36 +++++++++++++++++++ 3 files changed, 108 insertions(+) diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt index 9aed094779..9bd0874d58 100644 --- a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt @@ -7,6 +7,8 @@ import io.sentry.Breadcrumb import io.sentry.EventProcessor import io.sentry.FilterString import io.sentry.Hint +import io.sentry.IContinuousProfiler +import io.sentry.IProfileConverter import io.sentry.IScopes import io.sentry.ITransportFactory import io.sentry.Integration @@ -87,6 +89,7 @@ class SentryAutoConfigurationTest { AutoConfigurations.of( SentryAutoConfiguration::class.java, WebMvcAutoConfiguration::class.java, + SentryProfilerAutoConfiguration::class.java, ) ) @@ -1037,6 +1040,39 @@ class SentryAutoConfigurationTest { } } + @Test + fun `when AgentMarker is on the classpath and ContinuousProfiling is enabled IContinuousProfiler and IProfileConverter beans are created and set on options`() { + SentryIntegrationPackageStorage.getInstance().clearStorage() + contextRunner + .withPropertyValues( + "sentry.dsn=http://key@localhost/proj", + "sentry.traces-sample-rate=1.0", + "sentry.auto-init=false", + "debug=true", + ) + .run { + assertThat(it).hasSingleBean(IContinuousProfiler::class.java) + assertThat(it).hasSingleBean(IProfileConverter::class.java) + } + } + + @Test + fun `when AgentMarker is not on the classpath and ContinuousProfiling is enabled IContinuousProfiler and IProfileConverter beans are not created`() { + SentryIntegrationPackageStorage.getInstance().clearStorage() + contextRunner + .withPropertyValues( + "sentry.dsn=http://key@localhost/proj", + "sentry.traces-sample-rate=1.0", + "sentry.profile-session-sample-rate=1.0", + "debug=true", + ) + .withClassLoader(FilteredClassLoader(AgentMarker::class.java, OpenTelemetry::class.java)) + .run { + assertThat(it).doesNotHaveBean(IContinuousProfiler::class.java) + assertThat(it).doesNotHaveBean(IProfileConverter::class.java) + } + } + @Configuration(proxyBeanMethods = false) open class CustomSchedulerFactoryBeanCustomizerConfiguration { class MyJobListener : JobListener { diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt index fde201015f..da835d1026 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt @@ -8,6 +8,8 @@ import io.sentry.DataCategory import io.sentry.EventProcessor import io.sentry.FilterString import io.sentry.Hint +import io.sentry.IContinuousProfiler +import io.sentry.IProfileConverter import io.sentry.IScopes import io.sentry.ITransportFactory import io.sentry.Integration @@ -91,6 +93,7 @@ class SentryAutoConfigurationTest { AutoConfigurations.of( SentryAutoConfiguration::class.java, WebMvcAutoConfiguration::class.java, + SentryProfilerAutoConfiguration::class.java, ) ) @@ -1059,6 +1062,39 @@ class SentryAutoConfigurationTest { } } + @Test + fun `when AgentMarker is on the classpath and ContinuousProfiling is enabled IContinuousProfiler and IProfileConverter beans are created and set on options`() { + SentryIntegrationPackageStorage.getInstance().clearStorage() + contextRunner + .withPropertyValues( + "sentry.dsn=http://key@localhost/proj", + "sentry.traces-sample-rate=1.0", + "sentry.auto-init=false", + "debug=true", + ) + .run { + assertThat(it).hasSingleBean(IContinuousProfiler::class.java) + assertThat(it).hasSingleBean(IProfileConverter::class.java) + } + } + + @Test + fun `when AgentMarker is not on the classpath and ContinuousProfiling is enabled IContinuousProfiler and IProfileConverter beans are not created`() { + SentryIntegrationPackageStorage.getInstance().clearStorage() + contextRunner + .withPropertyValues( + "sentry.dsn=http://key@localhost/proj", + "sentry.traces-sample-rate=1.0", + "sentry.profile-session-sample-rate=1.0", + "debug=true", + ) + .withClassLoader(FilteredClassLoader(AgentMarker::class.java, OpenTelemetry::class.java)) + .run { + assertThat(it).doesNotHaveBean(IContinuousProfiler::class.java) + assertThat(it).doesNotHaveBean(IProfileConverter::class.java) + } + } + @Configuration(proxyBeanMethods = false) open class CustomSchedulerFactoryBeanCustomizerConfiguration { class MyJobListener : JobListener { diff --git a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt index 1d80b3b648..729d31936e 100644 --- a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt @@ -8,6 +8,8 @@ import io.sentry.DataCategory import io.sentry.EventProcessor import io.sentry.FilterString import io.sentry.Hint +import io.sentry.IContinuousProfiler +import io.sentry.IProfileConverter import io.sentry.IScopes import io.sentry.ITransportFactory import io.sentry.Integration @@ -90,6 +92,7 @@ class SentryAutoConfigurationTest { AutoConfigurations.of( SentryAutoConfiguration::class.java, WebMvcAutoConfiguration::class.java, + SentryProfilerAutoConfiguration::class.java, ) ) @@ -910,6 +913,39 @@ class SentryAutoConfigurationTest { } } + @Test + fun `when AgentMarker is on the classpath and ContinuousProfiling is enabled IContinuousProfiler and IProfileConverter beans are created and set on options`() { + SentryIntegrationPackageStorage.getInstance().clearStorage() + contextRunner + .withPropertyValues( + "sentry.dsn=http://key@localhost/proj", + "sentry.traces-sample-rate=1.0", + "sentry.auto-init=false", + "debug=true", + ) + .run { + assertThat(it).hasSingleBean(IContinuousProfiler::class.java) + assertThat(it).hasSingleBean(IProfileConverter::class.java) + } + } + + @Test + fun `when AgentMarker is not on the classpath and ContinuousProfiling is enabled IContinuousProfiler and IProfileConverter beans are not created`() { + SentryIntegrationPackageStorage.getInstance().clearStorage() + contextRunner + .withPropertyValues( + "sentry.dsn=http://key@localhost/proj", + "sentry.traces-sample-rate=1.0", + "sentry.profile-session-sample-rate=1.0", + "debug=true", + ) + .withClassLoader(FilteredClassLoader(AgentMarker::class.java, OpenTelemetry::class.java)) + .run { + assertThat(it).doesNotHaveBean(IContinuousProfiler::class.java) + assertThat(it).doesNotHaveBean(IProfileConverter::class.java) + } + } + @Test fun `creates quartz config`() { contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { From 5e75584f4efd4aafcfd7954c00209c214f487431 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Fri, 24 Oct 2025 09:18:31 +0200 Subject: [PATCH 07/13] bump api --- sentry-spring-7/api/sentry-spring-7.api | 6 ++++++ sentry-spring-boot-4/api/sentry-spring-boot-4.api | 4 ++++ sentry-spring-boot/api/sentry-spring-boot.api | 4 ++++ sentry-spring/api/sentry-spring.api | 6 ++++++ 4 files changed, 20 insertions(+) diff --git a/sentry-spring-7/api/sentry-spring-7.api b/sentry-spring-7/api/sentry-spring-7.api index cd17eab315..3a57c13e83 100644 --- a/sentry-spring-7/api/sentry-spring-7.api +++ b/sentry-spring-7/api/sentry-spring-7.api @@ -42,6 +42,12 @@ public class io/sentry/spring7/SentryInitBeanPostProcessor : org/springframework public fun setApplicationContext (Lorg/springframework/context/ApplicationContext;)V } +public class io/sentry/spring7/SentryProfilerConfiguration { + public fun ()V + public fun sentryOpenTelemetryProfilerConfiguration ()Lio/sentry/IContinuousProfiler; + public fun sentryOpenTelemetryProfilerConverterConfiguration ()Lio/sentry/IProfileConverter; +} + public class io/sentry/spring7/SentryRequestHttpServletRequestProcessor : io/sentry/EventProcessor { public fun (Lio/sentry/spring7/tracing/TransactionNameProvider;Ljakarta/servlet/http/HttpServletRequest;)V public fun getOrder ()Ljava/lang/Long; diff --git a/sentry-spring-boot-4/api/sentry-spring-boot-4.api b/sentry-spring-boot-4/api/sentry-spring-boot-4.api index 4eb01c46de..4c8be990b8 100644 --- a/sentry-spring-boot-4/api/sentry-spring-boot-4.api +++ b/sentry-spring-boot-4/api/sentry-spring-boot-4.api @@ -24,6 +24,10 @@ public class io/sentry/spring/boot4/SentryLogbackInitializer : org/springframewo public fun supportsEventType (Lorg/springframework/core/ResolvableType;)Z } +public class io/sentry/spring/boot4/SentryProfilerAutoConfiguration { + public fun ()V +} + public class io/sentry/spring/boot4/SentryProperties : io/sentry/SentryOptions { public fun ()V public fun getExceptionResolverOrder ()I diff --git a/sentry-spring-boot/api/sentry-spring-boot.api b/sentry-spring-boot/api/sentry-spring-boot.api index 6bebb6ed09..ef726c4fc2 100644 --- a/sentry-spring-boot/api/sentry-spring-boot.api +++ b/sentry-spring-boot/api/sentry-spring-boot.api @@ -24,6 +24,10 @@ public class io/sentry/spring/boot/SentryLogbackInitializer : org/springframewor public fun supportsEventType (Lorg/springframework/core/ResolvableType;)Z } +public class io/sentry/spring/boot/SentryProfilerAutoConfiguration { + public fun ()V +} + public class io/sentry/spring/boot/SentryProperties : io/sentry/SentryOptions { public fun ()V public fun getExceptionResolverOrder ()I diff --git a/sentry-spring/api/sentry-spring.api b/sentry-spring/api/sentry-spring.api index 467d96beec..fb07af382b 100644 --- a/sentry-spring/api/sentry-spring.api +++ b/sentry-spring/api/sentry-spring.api @@ -42,6 +42,12 @@ public class io/sentry/spring/SentryInitBeanPostProcessor : org/springframework/ public fun setApplicationContext (Lorg/springframework/context/ApplicationContext;)V } +public class io/sentry/spring/SentryProfilerConfiguration { + public fun ()V + public fun sentryOpenTelemetryProfilerConfiguration ()Lio/sentry/IContinuousProfiler; + public fun sentryOpenTelemetryProfilerConverterConfiguration ()Lio/sentry/IProfileConverter; +} + public class io/sentry/spring/SentryRequestHttpServletRequestProcessor : io/sentry/EventProcessor { public fun (Lio/sentry/spring/tracing/TransactionNameProvider;Ljavax/servlet/http/HttpServletRequest;)V public fun getOrder ()Ljava/lang/Long; From 0514903537d2e489e008eb45d30998acf25d8d77 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Mon, 3 Nov 2025 09:30:46 +0100 Subject: [PATCH 08/13] add missing package to test, adapt log statement --- .../io/sentry/asyncprofiler/init/AsyncProfilerInitUtilTest.kt | 2 ++ sentry/src/main/java/io/sentry/SentryEnvelopeItem.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/init/AsyncProfilerInitUtilTest.kt b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/init/AsyncProfilerInitUtilTest.kt index e8bfd30117..06b6d18f9b 100644 --- a/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/init/AsyncProfilerInitUtilTest.kt +++ b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/init/AsyncProfilerInitUtilTest.kt @@ -1,3 +1,5 @@ +package io.sentry.asyncprofiler.init + import io.sentry.ILogger import io.sentry.ISentryExecutorService import io.sentry.NoOpContinuousProfiler diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index 0dbc356161..04bf74fcfe 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -314,7 +314,7 @@ private static void ensureAttachmentSizeLimit( } } else { throw new SentryEnvelopeException( - "Could not load a ProfileConverter, dropping chunk."); + "No ProfileConverter available, dropping chunk."); } } else { // The payload of the profile item is a json including the trace file encoded with From 2cb26e168dfcd8cc41d08ef7aca87a01dcf81a9a Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Mon, 3 Nov 2025 09:47:01 +0100 Subject: [PATCH 09/13] add changelog entry, improve logs --- CHANGELOG.md | 4 ++++ .../main/java/io/sentry/util/InitUtil.java | 20 ++++++++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed643ca30e..f7d3d6b7c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Fixes + +- Fix profiling init for Spring and Spring Boot w Agent auto-init ([#4815](https://github.com/getsentry/sentry-java/pull/4815)) + ### Improvements - Fallback to distinct-id as user.id logging attribute when user is not set ([#4847](https://github.com/getsentry/sentry-java/pull/4847)) diff --git a/sentry/src/main/java/io/sentry/util/InitUtil.java b/sentry/src/main/java/io/sentry/util/InitUtil.java index 676b160a80..15f221f685 100644 --- a/sentry/src/main/java/io/sentry/util/InitUtil.java +++ b/sentry/src/main/java/io/sentry/util/InitUtil.java @@ -82,6 +82,13 @@ public static IContinuousProfiler initializeProfiler(@NotNull SentryOptions opti if (!(continuousProfiler instanceof NoOpContinuousProfiler)) { options.setContinuousProfiler(continuousProfiler); + options.getLogger().log(SentryLevel.INFO, "Successfully loaded profiler"); + } else { + options + .getLogger() + .log( + SentryLevel.WARNING, + "Could not load profiler, profiling will be disabled. If you are using Spring or Spring Boot with the OTEL Agent profiler init will be retried."); } return continuousProfiler; @@ -101,25 +108,20 @@ public static IProfileConverter initializeProfileConverter(@NotNull SentryOption if (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"); + } else { options .getLogger() .log( - SentryLevel.INFO, - "Successfully loaded profile converter via Spring Boot classloader"); + SentryLevel.WARNING, + "Could not load profile converter. If you are using Spring or Spring Boot with the OTEL Agent, profile converter init will be retried."); } } return converter; } - // TODO: Add initialization of profiler here } From 41d2c6a736beb99acf4fca8a4f1ff9d993790714 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 4 Nov 2025 12:22:46 +0100 Subject: [PATCH 10/13] Update sentry-spring-7/src/main/java/io/sentry/spring7/SentryProfilerConfiguration.java Co-authored-by: Alexander Dinauer --- .../java/io/sentry/spring7/SentryProfilerConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/SentryProfilerConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryProfilerConfiguration.java index 939d5df98d..64e4479c88 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/SentryProfilerConfiguration.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryProfilerConfiguration.java @@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration; /** - * Handles late initialization of the profiler if the application is run with the OTEL Agent in + * Handles late initialization of the profiler if the application is run with the Opentelemetry 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. From 87071dd524670d117010a9840d924e899391ee65 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 4 Nov 2025 11:25:45 +0000 Subject: [PATCH 11/13] Format code --- .../io/sentry/spring7/SentryProfilerConfiguration.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/SentryProfilerConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryProfilerConfiguration.java index 64e4479c88..eefdce75a9 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/SentryProfilerConfiguration.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryProfilerConfiguration.java @@ -13,10 +13,10 @@ import org.springframework.context.annotation.Configuration; /** - * Handles late initialization of the profiler if the application is run with the Opentelemetry 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. + * Handles late initialization of the profiler if the application is run with the Opentelemetry + * 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 From 8fca29a638fcec2baa7c2aaebf138c3cfc59d49c Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 4 Nov 2025 14:47:44 +0100 Subject: [PATCH 12/13] make initUtil methods more readable, return value from options, improve tests --- .../init/AsyncProfilerInitUtilTest.kt | 38 +++--- .../main/java/io/sentry/util/InitUtil.java | 123 ++++++++++-------- 2 files changed, 82 insertions(+), 79 deletions(-) diff --git a/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/init/AsyncProfilerInitUtilTest.kt b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/init/AsyncProfilerInitUtilTest.kt index 06b6d18f9b..7d2379ca64 100644 --- a/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/init/AsyncProfilerInitUtilTest.kt +++ b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/init/AsyncProfilerInitUtilTest.kt @@ -9,6 +9,7 @@ import io.sentry.asyncprofiler.profiling.JavaContinuousProfiler import io.sentry.asyncprofiler.provider.AsyncProfilerProfileConverterProvider import io.sentry.util.InitUtil import kotlin.test.Test +import kotlin.test.assertSame import org.mockito.kotlin.mock class AsyncProfilerInitUtilTest { @@ -21,59 +22,52 @@ class AsyncProfilerInitUtilTest { } @Test - fun `initialize Converter returns no-op profiler if profiling disabled`() { + fun `initialize Converter returns no-op converter if profiling disabled`() { val options = SentryOptions() val converter = InitUtil.initializeProfileConverter(options) assert(converter is NoOpProfileConverter) } @Test - fun `initialize Profiler returns no-op profiler if profiler already initialized`() { + fun `initialize profiler returns the existing profiler from options if already initialized`() { + val initialProfiler = + JavaContinuousProfiler(mock(), "", 10, mock()) val options = SentryOptions().also { it.setProfileSessionSampleRate(1.0) - it.tracesSampleRate = 1.0 - it.setContinuousProfiler( - JavaContinuousProfiler(mock(), "", 10, mock()) - ) + it.setContinuousProfiler(initialProfiler) } val profiler = InitUtil.initializeProfiler(options) - assert(profiler is NoOpContinuousProfiler) + assertSame(initialProfiler, profiler) } @Test - fun `initialize converter returns no-op converter if converter already initialized`() { + fun `initialize converter returns the existing converter from options if already initialized`() { + val initialConverter = AsyncProfilerProfileConverterProvider.AsyncProfilerProfileConverter() val options = SentryOptions().also { it.setProfileSessionSampleRate(1.0) - it.tracesSampleRate = 1.0 - it.profilerConverter = AsyncProfilerProfileConverterProvider.AsyncProfilerProfileConverter() + it.profilerConverter = initialConverter } val converter = InitUtil.initializeProfileConverter(options) - assert(converter is NoOpProfileConverter) + assertSame(initialConverter, converter) } @Test fun `initialize Profiler returns JavaContinuousProfiler if profiling enabled but profiler not yet initialized`() { - val options = - SentryOptions().also { - it.setProfileSessionSampleRate(1.0) - it.tracesSampleRate = 1.0 - } + val options = SentryOptions().also { it.setProfileSessionSampleRate(1.0) } val profiler = InitUtil.initializeProfiler(options) + assertSame(profiler, options.continuousProfiler) assert(profiler is JavaContinuousProfiler) } @Test - fun `initialize Profiler returns AsyncProfilerProfileConverterProvider if profiling enabled but profiler not yet initialized`() { - val options = - SentryOptions().also { - it.setProfileSessionSampleRate(1.0) - it.tracesSampleRate = 1.0 - } + fun `initialize Converter returns AsyncProfilerProfileConverterProvider if profiling enabled but profiler not yet initialized`() { + val options = SentryOptions().also { it.setProfileSessionSampleRate(1.0) } val converter = InitUtil.initializeProfileConverter(options) + assertSame(converter, options.profilerConverter) assert(converter is AsyncProfilerProfileConverterProvider.AsyncProfilerProfileConverter) } } diff --git a/sentry/src/main/java/io/sentry/util/InitUtil.java b/sentry/src/main/java/io/sentry/util/InitUtil.java index 15f221f685..a43715fff2 100644 --- a/sentry/src/main/java/io/sentry/util/InitUtil.java +++ b/sentry/src/main/java/io/sentry/util/InitUtil.java @@ -54,74 +54,83 @@ public static boolean shouldInit( } public static IContinuousProfiler initializeProfiler(@NotNull SentryOptions options) { - IContinuousProfiler continuousProfiler = NoOpContinuousProfiler.getInstance(); - - if (options.isContinuousProfilingEnabled() - && options.getContinuousProfiler() == NoOpContinuousProfiler.getInstance()) { - try { - String profilingTracesDirPath = options.getProfilingTracesDirPath(); - if (profilingTracesDirPath == null) { - File tempDir = new File(System.getProperty("java.io.tmpdir"), "sentry_profiling_traces"); - boolean createDirectorySuccess = tempDir.mkdirs() || tempDir.exists(); - - if (!createDirectorySuccess) { - throw new IllegalArgumentException( - "Creating a fallback directory for profiling failed in " - + tempDir.getAbsolutePath()); - } - profilingTracesDirPath = tempDir.getAbsolutePath(); - options.setProfilingTracesDirPath(profilingTracesDirPath); - } - - continuousProfiler = - ProfilingServiceLoader.loadContinuousProfiler( - options.getLogger(), - profilingTracesDirPath, - options.getProfilingTracesHz(), - options.getExecutorService()); - - if (!(continuousProfiler instanceof NoOpContinuousProfiler)) { - options.setContinuousProfiler(continuousProfiler); - options.getLogger().log(SentryLevel.INFO, "Successfully loaded profiler"); - } else { - options - .getLogger() - .log( - SentryLevel.WARNING, - "Could not load profiler, profiling will be disabled. If you are using Spring or Spring Boot with the OTEL Agent profiler init will be retried."); - } - - return continuousProfiler; - - } catch (Exception e) { + if (!shouldInitializeProfiler(options)) { + return options.getContinuousProfiler(); + } + + try { + String profilingTracesDirPath = getOrCreateProfilingTracesDir(options); + IContinuousProfiler profiler = + ProfilingServiceLoader.loadContinuousProfiler( + options.getLogger(), + profilingTracesDirPath, + options.getProfilingTracesHz(), + options.getExecutorService()); + + if (profiler instanceof NoOpContinuousProfiler) { options .getLogger() - .log(SentryLevel.ERROR, "Failed to create default profiling traces directory", e); + .log( + SentryLevel.WARNING, + "Could not load profiler, profiling will be disabled. If you are using Spring or Spring Boot with the OTEL Agent profiler init will be retried."); + } else { + options.setContinuousProfiler(profiler); + options.getLogger().log(SentryLevel.INFO, "Successfully loaded profiler"); } + } catch (Exception e) { + options + .getLogger() + .log(SentryLevel.ERROR, "Failed to create default profiling traces directory", e); } - return continuousProfiler; + + return options.getContinuousProfiler(); } public static IProfileConverter initializeProfileConverter(@NotNull SentryOptions options) { - IProfileConverter converter = NoOpProfileConverter.getInstance(); - - if (options.isContinuousProfilingEnabled() - && options.getProfilerConverter() instanceof NoOpProfileConverter) { + if (!shouldInitializeProfileConverter(options)) { + return options.getProfilerConverter(); + } - converter = ProfilingServiceLoader.loadProfileConverter(); + IProfileConverter converter = ProfilingServiceLoader.loadProfileConverter(); + if (converter instanceof NoOpProfileConverter) { + options + .getLogger() + .log( + SentryLevel.WARNING, + "Could not load profile converter. If you are using Spring or Spring Boot with the OTEL Agent, profile converter init will be retried."); + } else { options.setProfilerConverter(converter); + options.getLogger().log(SentryLevel.INFO, "Successfully loaded profile converter"); + } - if (!(converter instanceof NoOpProfileConverter)) { - options.getLogger().log(SentryLevel.INFO, "Successfully loaded profile converter"); - } else { - options - .getLogger() - .log( - SentryLevel.WARNING, - "Could not load profile converter. If you are using Spring or Spring Boot with the OTEL Agent, profile converter init will be retried."); - } + return options.getProfilerConverter(); + } + + private static boolean shouldInitializeProfiler(@NotNull SentryOptions options) { + return options.isContinuousProfilingEnabled() + && options.getContinuousProfiler() instanceof NoOpContinuousProfiler; + } + + private static boolean shouldInitializeProfileConverter(@NotNull SentryOptions options) { + return options.isContinuousProfilingEnabled() + && options.getProfilerConverter() instanceof NoOpProfileConverter; + } + + private static String getOrCreateProfilingTracesDir(@NotNull SentryOptions options) { + String profilingTracesDirPath = options.getProfilingTracesDirPath(); + if (profilingTracesDirPath != null) { + return profilingTracesDirPath; } - return converter; + + File tempDir = new File(System.getProperty("java.io.tmpdir"), "sentry_profiling_traces"); + if (!tempDir.mkdirs() && !tempDir.exists()) { + throw new IllegalArgumentException( + "Creating a fallback directory for profiling failed in " + tempDir.getAbsolutePath()); + } + + profilingTracesDirPath = tempDir.getAbsolutePath(); + options.setProfilingTracesDirPath(profilingTracesDirPath); + return profilingTracesDirPath; } } From 4a52a0de90f26b87dfa018504ee84f5e183d36fd Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Wed, 5 Nov 2025 10:05:11 +0100 Subject: [PATCH 13/13] add tests for path creation --- .../init/AsyncProfilerInitUtilTest.kt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/init/AsyncProfilerInitUtilTest.kt b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/init/AsyncProfilerInitUtilTest.kt index 7d2379ca64..614e940021 100644 --- a/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/init/AsyncProfilerInitUtilTest.kt +++ b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/init/AsyncProfilerInitUtilTest.kt @@ -9,6 +9,7 @@ import io.sentry.asyncprofiler.profiling.JavaContinuousProfiler import io.sentry.asyncprofiler.provider.AsyncProfilerProfileConverterProvider import io.sentry.util.InitUtil import kotlin.test.Test +import kotlin.test.assertNotNull import kotlin.test.assertSame import org.mockito.kotlin.mock @@ -70,4 +71,26 @@ class AsyncProfilerInitUtilTest { assertSame(converter, options.profilerConverter) assert(converter is AsyncProfilerProfileConverterProvider.AsyncProfilerProfileConverter) } + + @Test + fun `initialize profiler uses existing profilingTracesDirPath when set`() { + val customPath = "/custom/path/to/traces" + val options = + SentryOptions().also { + it.setProfileSessionSampleRate(1.0) + it.profilingTracesDirPath = customPath + } + val profiler = InitUtil.initializeProfiler(options) + assert(profiler is JavaContinuousProfiler) + assertSame(customPath, options.profilingTracesDirPath) + } + + @Test + fun `initialize profiler creates and sets profilingTracesDirPath when null`() { + val options = SentryOptions().also { it.setProfileSessionSampleRate(1.0) } + val profiler = InitUtil.initializeProfiler(options) + assert(profiler is JavaContinuousProfiler) + assertNotNull(options.profilingTracesDirPath) + assert(options.profilingTracesDirPath!!.contains("sentry_profiling_traces")) + } }