Skip to content

Commit 591b401

Browse files
lbloderadinauergetsentry-bot
authored
Fix profiling init for Spring and Spring Boot w Agent auto-init (#4815)
Co-authored-by: Alexander Dinauer <adinauer@users.noreply.github.com> Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>
1 parent e990937 commit 591b401

File tree

34 files changed

+857
-49
lines changed

34 files changed

+857
-49
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- Session Replay: Cache connection status instead of using blocking calls ([#4891](https://github.com/getsentry/sentry-java/pull/4891))
1212
- Fix log count in client reports ([#4869](https://github.com/getsentry/sentry-java/pull/4869))
1313
- Fix profilerId propagation ([#4833](https://github.com/getsentry/sentry-java/pull/4833))
14+
- Fix profiling init for Spring and Spring Boot w Agent auto-init ([#4815](https://github.com/getsentry/sentry-java/pull/4815))
1415

1516
### Improvements
1617

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import io.sentry.profiling.JavaProfileConverterProvider;
66
import org.jetbrains.annotations.ApiStatus;
77
import org.jetbrains.annotations.NotNull;
8-
import org.jetbrains.annotations.Nullable;
98

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

1716
@Override
18-
public @Nullable IProfileConverter getProfileConverter() {
17+
public @NotNull IProfileConverter getProfileConverter() {
1918
return new AsyncProfilerProfileConverter();
2019
}
2120

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package io.sentry.asyncprofiler.init
2+
3+
import io.sentry.ILogger
4+
import io.sentry.ISentryExecutorService
5+
import io.sentry.NoOpContinuousProfiler
6+
import io.sentry.NoOpProfileConverter
7+
import io.sentry.SentryOptions
8+
import io.sentry.asyncprofiler.profiling.JavaContinuousProfiler
9+
import io.sentry.asyncprofiler.provider.AsyncProfilerProfileConverterProvider
10+
import io.sentry.util.InitUtil
11+
import kotlin.test.Test
12+
import kotlin.test.assertNotNull
13+
import kotlin.test.assertSame
14+
import org.mockito.kotlin.mock
15+
16+
class AsyncProfilerInitUtilTest {
17+
18+
@Test
19+
fun `initialize Profiler returns no-op profiler if profiling disabled`() {
20+
val options = SentryOptions()
21+
val profiler = InitUtil.initializeProfiler(options)
22+
assert(profiler is NoOpContinuousProfiler)
23+
}
24+
25+
@Test
26+
fun `initialize Converter returns no-op converter if profiling disabled`() {
27+
val options = SentryOptions()
28+
val converter = InitUtil.initializeProfileConverter(options)
29+
assert(converter is NoOpProfileConverter)
30+
}
31+
32+
@Test
33+
fun `initialize profiler returns the existing profiler from options if already initialized`() {
34+
val initialProfiler =
35+
JavaContinuousProfiler(mock<ILogger>(), "", 10, mock<ISentryExecutorService>())
36+
val options =
37+
SentryOptions().also {
38+
it.setProfileSessionSampleRate(1.0)
39+
it.setContinuousProfiler(initialProfiler)
40+
}
41+
42+
val profiler = InitUtil.initializeProfiler(options)
43+
assertSame(initialProfiler, profiler)
44+
}
45+
46+
@Test
47+
fun `initialize converter returns the existing converter from options if already initialized`() {
48+
val initialConverter = AsyncProfilerProfileConverterProvider.AsyncProfilerProfileConverter()
49+
val options =
50+
SentryOptions().also {
51+
it.setProfileSessionSampleRate(1.0)
52+
it.profilerConverter = initialConverter
53+
}
54+
55+
val converter = InitUtil.initializeProfileConverter(options)
56+
assertSame(initialConverter, converter)
57+
}
58+
59+
@Test
60+
fun `initialize Profiler returns JavaContinuousProfiler if profiling enabled but profiler not yet initialized`() {
61+
val options = SentryOptions().also { it.setProfileSessionSampleRate(1.0) }
62+
val profiler = InitUtil.initializeProfiler(options)
63+
assertSame(profiler, options.continuousProfiler)
64+
assert(profiler is JavaContinuousProfiler)
65+
}
66+
67+
@Test
68+
fun `initialize Converter returns AsyncProfilerProfileConverterProvider if profiling enabled but profiler not yet initialized`() {
69+
val options = SentryOptions().also { it.setProfileSessionSampleRate(1.0) }
70+
val converter = InitUtil.initializeProfileConverter(options)
71+
assertSame(converter, options.profilerConverter)
72+
assert(converter is AsyncProfilerProfileConverterProvider.AsyncProfilerProfileConverter)
73+
}
74+
75+
@Test
76+
fun `initialize profiler uses existing profilingTracesDirPath when set`() {
77+
val customPath = "/custom/path/to/traces"
78+
val options =
79+
SentryOptions().also {
80+
it.setProfileSessionSampleRate(1.0)
81+
it.profilingTracesDirPath = customPath
82+
}
83+
val profiler = InitUtil.initializeProfiler(options)
84+
assert(profiler is JavaContinuousProfiler)
85+
assertSame(customPath, options.profilingTracesDirPath)
86+
}
87+
88+
@Test
89+
fun `initialize profiler creates and sets profilingTracesDirPath when null`() {
90+
val options = SentryOptions().also { it.setProfileSessionSampleRate(1.0) }
91+
val profiler = InitUtil.initializeProfiler(options)
92+
assert(profiler is JavaContinuousProfiler)
93+
assertNotNull(options.profilingTracesDirPath)
94+
assert(options.profilingTracesDirPath!!.contains("sentry_profiling_traces"))
95+
}
96+
}

sentry-spring-7/api/sentry-spring-7.api

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ public class io/sentry/spring7/SentryInitBeanPostProcessor : org/springframework
4242
public fun setApplicationContext (Lorg/springframework/context/ApplicationContext;)V
4343
}
4444

45+
public class io/sentry/spring7/SentryProfilerConfiguration {
46+
public fun <init> ()V
47+
public fun sentryOpenTelemetryProfilerConfiguration ()Lio/sentry/IContinuousProfiler;
48+
public fun sentryOpenTelemetryProfilerConverterConfiguration ()Lio/sentry/IProfileConverter;
49+
}
50+
4551
public class io/sentry/spring7/SentryRequestHttpServletRequestProcessor : io/sentry/EventProcessor {
4652
public fun <init> (Lio/sentry/spring7/tracing/TransactionNameProvider;Ljakarta/servlet/http/HttpServletRequest;)V
4753
public fun getOrder ()Ljava/lang/Long;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package io.sentry.spring7;
2+
3+
import com.jakewharton.nopen.annotation.Open;
4+
import io.sentry.IContinuousProfiler;
5+
import io.sentry.IProfileConverter;
6+
import io.sentry.NoOpContinuousProfiler;
7+
import io.sentry.NoOpProfileConverter;
8+
import io.sentry.Sentry;
9+
import io.sentry.SentryOptions;
10+
import io.sentry.util.InitUtil;
11+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
12+
import org.springframework.context.annotation.Bean;
13+
import org.springframework.context.annotation.Configuration;
14+
15+
/**
16+
* Handles late initialization of the profiler if the application is run with the Opentelemetry
17+
* Agent in auto-init mode. In that case the agent cannot initialize the profiler yet and falls back
18+
* to No-Op implementations. This Configuration sets the profiler and converter on the options if
19+
* that was the case.
20+
*/
21+
@Configuration(proxyBeanMethods = false)
22+
@Open
23+
public class SentryProfilerConfiguration {
24+
25+
@Bean
26+
@ConditionalOnMissingBean(name = "sentryOpenTelemetryProfilerConfiguration")
27+
public IContinuousProfiler sentryOpenTelemetryProfilerConfiguration() {
28+
SentryOptions options = Sentry.getGlobalScope().getOptions();
29+
IContinuousProfiler profiler = NoOpContinuousProfiler.getInstance();
30+
31+
if (Sentry.isEnabled()) {
32+
return InitUtil.initializeProfiler(options);
33+
} else {
34+
return profiler;
35+
}
36+
}
37+
38+
@Bean
39+
@ConditionalOnMissingBean(name = "sentryOpenTelemetryProfilerConverterConfiguration")
40+
public IProfileConverter sentryOpenTelemetryProfilerConverterConfiguration() {
41+
SentryOptions options = Sentry.getGlobalScope().getOptions();
42+
IProfileConverter converter = NoOpProfileConverter.getInstance();
43+
44+
if (Sentry.isEnabled()) {
45+
return InitUtil.initializeProfileConverter(options);
46+
} else {
47+
return converter;
48+
}
49+
}
50+
}

sentry-spring-boot-4/api/sentry-spring-boot-4.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ public class io/sentry/spring/boot4/SentryLogbackInitializer : org/springframewo
2424
public fun supportsEventType (Lorg/springframework/core/ResolvableType;)Z
2525
}
2626

27+
public class io/sentry/spring/boot4/SentryProfilerAutoConfiguration {
28+
public fun <init> ()V
29+
}
30+
2731
public class io/sentry/spring/boot4/SentryProperties : io/sentry/SentryOptions {
2832
public fun <init> ()V
2933
public fun getExceptionResolverOrder ()I

sentry-spring-boot-4/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ dependencies {
8585
testImplementation(libs.okhttp.mockwebserver)
8686
testImplementation(libs.otel)
8787
testImplementation(libs.otel.extension.autoconfigure.spi)
88+
testImplementation(projects.sentryAsyncProfiler)
8889
/**
8990
* Adding a version of opentelemetry-spring-boot-starter that doesn't support Spring Boot 4 causes
9091
* java.lang.IllegalArgumentException: Could not find class
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package io.sentry.spring.boot4;
2+
3+
import com.jakewharton.nopen.annotation.Open;
4+
import io.sentry.spring7.SentryProfilerConfiguration;
5+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
6+
import org.springframework.context.annotation.Configuration;
7+
import org.springframework.context.annotation.Import;
8+
9+
@Configuration(proxyBeanMethods = false)
10+
@ConditionalOnClass(
11+
name = {
12+
"io.sentry.opentelemetry.agent.AgentMarker",
13+
"io.sentry.asyncprofiler.profiling.JavaContinuousProfiler"
14+
})
15+
@Open
16+
@Import(SentryProfilerConfiguration.class)
17+
public class SentryProfilerAutoConfiguration {}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
io.sentry.spring.boot4.SentryAutoConfiguration
2+
io.sentry.spring.boot4.SentryProfilerAutoConfiguration
23
io.sentry.spring.boot4.SentryLogbackAppenderAutoConfiguration
34
io.sentry.spring.boot4.SentryWebfluxAutoConfiguration

sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@ import io.sentry.Breadcrumb
77
import io.sentry.EventProcessor
88
import io.sentry.FilterString
99
import io.sentry.Hint
10+
import io.sentry.IContinuousProfiler
11+
import io.sentry.IProfileConverter
1012
import io.sentry.IScopes
1113
import io.sentry.ITransportFactory
1214
import io.sentry.Integration
15+
import io.sentry.NoOpContinuousProfiler
16+
import io.sentry.NoOpProfileConverter
1317
import io.sentry.NoOpTransportFactory
1418
import io.sentry.SamplingContext
1519
import io.sentry.Sentry
@@ -18,6 +22,8 @@ import io.sentry.SentryIntegrationPackageStorage
1822
import io.sentry.SentryLevel
1923
import io.sentry.SentryLogEvent
2024
import io.sentry.SentryOptions
25+
import io.sentry.asyncprofiler.profiling.JavaContinuousProfiler
26+
import io.sentry.asyncprofiler.provider.AsyncProfilerProfileConverterProvider
2127
import io.sentry.checkEvent
2228
import io.sentry.opentelemetry.SentryAutoConfigurationCustomizerProvider
2329
import io.sentry.opentelemetry.agent.AgentMarker
@@ -45,6 +51,7 @@ import kotlin.test.assertFalse
4551
import kotlin.test.assertTrue
4652
import org.aspectj.lang.ProceedingJoinPoint
4753
import org.assertj.core.api.Assertions.assertThat
54+
import org.mockito.internal.util.MockUtil.isMock
4855
import org.mockito.kotlin.any
4956
import org.mockito.kotlin.anyOrNull
5057
import org.mockito.kotlin.mock
@@ -87,6 +94,7 @@ class SentryAutoConfigurationTest {
8794
AutoConfigurations.of(
8895
SentryAutoConfiguration::class.java,
8996
WebMvcAutoConfiguration::class.java,
97+
SentryProfilerAutoConfiguration::class.java,
9098
)
9199
)
92100

@@ -1037,6 +1045,110 @@ class SentryAutoConfigurationTest {
10371045
}
10381046
}
10391047

1048+
@Test
1049+
fun `when AgentMarker is on the classpath and ContinuousProfiling is enabled IContinuousProfiler and IProfileConverter beans are created and set on options`() {
1050+
SentryIntegrationPackageStorage.getInstance().clearStorage()
1051+
contextRunner
1052+
.withPropertyValues(
1053+
"sentry.dsn=http://key@localhost/proj",
1054+
"sentry.profile-session-sample-rate=1.0",
1055+
)
1056+
.run {
1057+
assertThat(it).hasSingleBean(IContinuousProfiler::class.java)
1058+
assertThat(it).hasSingleBean(IProfileConverter::class.java)
1059+
assertThat(it)
1060+
.getBean(IProfileConverter::class.java)
1061+
.isInstanceOf(
1062+
AsyncProfilerProfileConverterProvider.AsyncProfilerProfileConverter::class.java
1063+
)
1064+
assertThat(it)
1065+
.getBean(IContinuousProfiler::class.java)
1066+
.isInstanceOf(JavaContinuousProfiler::class.java)
1067+
assertThat(it)
1068+
.getBean(IProfileConverter::class.java)
1069+
.isSameAs(Sentry.getGlobalScope().options.profilerConverter)
1070+
assertThat(it)
1071+
.getBean(IContinuousProfiler::class.java)
1072+
.isSameAs(Sentry.getGlobalScope().options.continuousProfiler)
1073+
}
1074+
}
1075+
1076+
@Test
1077+
fun `when AgentMarker is on the classpath and ContinuousProfiling is enabled IContinuousProfiler and IProfileConverter exist beans are taken from options`() {
1078+
SentryIntegrationPackageStorage.getInstance().clearStorage()
1079+
1080+
contextRunner
1081+
.withPropertyValues(
1082+
"sentry.dsn=http://key@localhost/proj",
1083+
"sentry.profile-session-sample-rate=1.0",
1084+
"sentry.auto-init=false",
1085+
"debug=true",
1086+
)
1087+
.withUserConfiguration(CustomProfilerOptionsConfigurationConfiguration::class.java)
1088+
.run {
1089+
val profiler = it.getBean(IContinuousProfiler::class.java)
1090+
assertTrue(isMock(profiler))
1091+
assertThat(it).hasSingleBean(IContinuousProfiler::class.java)
1092+
assertThat(it).hasSingleBean(IProfileConverter::class.java)
1093+
assertThat(it)
1094+
.getBean(IProfileConverter::class.java)
1095+
.isSameAs(Sentry.getGlobalScope().options.profilerConverter)
1096+
assertThat(it)
1097+
.getBean(IContinuousProfiler::class.java)
1098+
.isSameAs(Sentry.getGlobalScope().options.continuousProfiler)
1099+
}
1100+
}
1101+
1102+
@Test
1103+
fun `when AgentMarker is on the classpath and ContinuousProfiling is disabled NoOp Beans are created`() {
1104+
SentryIntegrationPackageStorage.getInstance().clearStorage()
1105+
1106+
contextRunner
1107+
.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.auto-init=false")
1108+
.run {
1109+
assertThat(it).hasSingleBean(IContinuousProfiler::class.java)
1110+
assertThat(it).hasSingleBean(IProfileConverter::class.java)
1111+
assertThat(it)
1112+
.getBean(IProfileConverter::class.java)
1113+
.isInstanceOf(NoOpProfileConverter::class.java)
1114+
assertThat(it)
1115+
.getBean(IContinuousProfiler::class.java)
1116+
.isInstanceOf(NoOpContinuousProfiler::class.java)
1117+
}
1118+
}
1119+
1120+
@Test
1121+
fun `when AgentMarker is not on the classpath and ContinuousProfiling is enabled IContinuousProfiler and IProfileConverter beans are not created`() {
1122+
SentryIntegrationPackageStorage.getInstance().clearStorage()
1123+
contextRunner
1124+
.withPropertyValues(
1125+
"sentry.dsn=http://key@localhost/proj",
1126+
"sentry.profile-session-sample-rate=1.0",
1127+
"debug=true",
1128+
)
1129+
.withClassLoader(FilteredClassLoader(AgentMarker::class.java, OpenTelemetry::class.java))
1130+
.run {
1131+
assertThat(it).doesNotHaveBean(IContinuousProfiler::class.java)
1132+
assertThat(it).doesNotHaveBean(IProfileConverter::class.java)
1133+
}
1134+
}
1135+
1136+
@Test
1137+
fun `when JavaContinuousProfiler is not on the classpath and ContinuousProfiling is enabled IProfileConverter beans are not created`() {
1138+
SentryIntegrationPackageStorage.getInstance().clearStorage()
1139+
contextRunner
1140+
.withPropertyValues(
1141+
"sentry.dsn=http://key@localhost/proj",
1142+
"sentry.profile-session-sample-rate=1.0",
1143+
"debug=true",
1144+
)
1145+
.withClassLoader(FilteredClassLoader(JavaContinuousProfiler::class.java))
1146+
.run {
1147+
assertThat(it).doesNotHaveBean(IContinuousProfiler::class.java)
1148+
assertThat(it).doesNotHaveBean(IProfileConverter::class.java)
1149+
}
1150+
}
1151+
10401152
@Configuration(proxyBeanMethods = false)
10411153
open class CustomSchedulerFactoryBeanCustomizerConfiguration {
10421154
class MyJobListener : JobListener {
@@ -1082,6 +1194,17 @@ class SentryAutoConfigurationTest {
10821194
@Bean open fun sentryOptionsConfiguration() = Sentry.OptionsConfiguration<SentryOptions> {}
10831195
}
10841196

1197+
@Configuration(proxyBeanMethods = false)
1198+
open class CustomProfilerOptionsConfigurationConfiguration {
1199+
private val profiler = mock<IContinuousProfiler>()
1200+
1201+
@Bean
1202+
open fun customOptionsConfiguration() =
1203+
Sentry.OptionsConfiguration<SentryOptions> { it.setContinuousProfiler(profiler) }
1204+
1205+
@Bean open fun beforeSendCallback() = CustomBeforeSendCallback()
1206+
}
1207+
10851208
@Configuration(proxyBeanMethods = false)
10861209
open class MockTransportConfiguration {
10871210

0 commit comments

Comments
 (0)