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 b8aa9111fae..a106cffc7dc 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-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 00000000000..e8bfd30117e --- /dev/null +++ b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/init/AsyncProfilerInitUtilTest.kt @@ -0,0 +1,77 @@ +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.asyncprofiler.provider.AsyncProfilerProfileConverterProvider +import io.sentry.util.InitUtil +import kotlin.test.Test +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) + } +} diff --git a/sentry-spring-7/api/sentry-spring-7.api b/sentry-spring-7/api/sentry-spring-7.api index cd17eab315e..3a57c13e835 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-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 00000000000..939d5df98da --- /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/api/sentry-spring-boot-4.api b/sentry-spring-boot-4/api/sentry-spring-boot-4.api index 4eb01c46ded..4c8be990b85 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-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 00000000000..e8338c7e824 --- /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 4697c6b6a9d..a108fa2ca10 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-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 9aed094779b..9bd0874d586 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/api/sentry-spring-boot-jakarta.api b/sentry-spring-boot-jakarta/api/sentry-spring-boot-jakarta.api index b0ef970d7d3..197bdbeef72 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 00000000000..8108e2cc02b --- /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 41436fe883f..1a812670017 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-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 fde201015f3..da835d10263 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/api/sentry-spring-boot.api b/sentry-spring-boot/api/sentry-spring-boot.api index 6bebb6ed095..ef726c4fc25 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-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 00000000000..952f842165b --- /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 9712f4b4077..5b27df50bb4 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-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt index 1d80b3b6480..729d31936e1 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 { diff --git a/sentry-spring-jakarta/api/sentry-spring-jakarta.api b/sentry-spring-jakarta/api/sentry-spring-jakarta.api index 3c1db200cbb..f28f4153b59 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 00000000000..9faf15466d2 --- /dev/null +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/SentryProfilerConfiguration.java @@ -0,0 +1,50 @@ +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.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/api/sentry-spring.api b/sentry-spring/api/sentry-spring.api index 467d96beec0..fb07af382ba 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; 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 00000000000..9bb7b713b52 --- /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; + } + } +} diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 3964619e0b9..802aa54089a 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 @@ -7044,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/NoOpProfileConverter.java b/sentry/src/main/java/io/sentry/NoOpProfileConverter.java new file mode 100644 index 00000000000..5a733a6697c --- /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 a28ee936e62..73231b9f5c3 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,38 +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); - } 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/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index bfcf4e780be..f4127e86ec2 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 c3b77679ec5..0dbc3561611 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 3a81b268b25..79af3df662a 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 e1fcdfa8793..69488f60216 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 c09a8ea3019..cf86a13fc53 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/main/java/io/sentry/util/InitUtil.java b/sentry/src/main/java/io/sentry/util/InitUtil.java index b598f51d861..676b160a802 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 } diff --git a/sentry/src/test/java/io/sentry/profiling/ProfilingServiceLoaderTest.kt b/sentry/src/test/java/io/sentry/profiling/ProfilingServiceLoaderTest.kt index ddbae8c8f9e..0fed85995da 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() } }