Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import io.sentry.profiling.JavaProfileConverterProvider;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

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

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package io.sentry.asyncprofiler.init

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 kotlin.test.assertNotNull
import kotlin.test.assertSame
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 converter if profiling disabled`() {
val options = SentryOptions()
val converter = InitUtil.initializeProfileConverter(options)
assert(converter is NoOpProfileConverter)
}

@Test
fun `initialize profiler returns the existing profiler from options if already initialized`() {
val initialProfiler =
JavaContinuousProfiler(mock<ILogger>(), "", 10, mock<ISentryExecutorService>())
val options =
SentryOptions().also {
it.setProfileSessionSampleRate(1.0)
it.setContinuousProfiler(initialProfiler)
}

val profiler = InitUtil.initializeProfiler(options)
assertSame(initialProfiler, profiler)
}

@Test
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.profilerConverter = initialConverter
}

val converter = InitUtil.initializeProfileConverter(options)
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) }
val profiler = InitUtil.initializeProfiler(options)
assertSame(profiler, options.continuousProfiler)
assert(profiler is JavaContinuousProfiler)
}

@Test
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)
}

@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"))
}
}
6 changes: 6 additions & 0 deletions sentry-spring-7/api/sentry-spring-7.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> ()V
public fun sentryOpenTelemetryProfilerConfiguration ()Lio/sentry/IContinuousProfiler;
public fun sentryOpenTelemetryProfilerConverterConfiguration ()Lio/sentry/IProfileConverter;
}

public class io/sentry/spring7/SentryRequestHttpServletRequestProcessor : io/sentry/EventProcessor {
public fun <init> (Lio/sentry/spring7/tracing/TransactionNameProvider;Ljakarta/servlet/http/HttpServletRequest;)V
public fun getOrder ()Ljava/lang/Long;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,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 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
public class SentryProfilerConfiguration {

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

Choose a reason for hiding this comment

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

Is there an expected order to these bean evaluations?

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;
}
}
}
4 changes: 4 additions & 0 deletions sentry-spring-boot-4/api/sentry-spring-boot-4.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> ()V
}

public class io/sentry/spring/boot4/SentryProperties : io/sentry/SentryOptions {
public fun <init> ()V
public fun getExceptionResolverOrder ()I
Expand Down
1 change: 1 addition & 0 deletions sentry-spring-boot-4/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ dependencies {
testImplementation(libs.okhttp.mockwebserver)
testImplementation(libs.otel)
testImplementation(libs.otel.extension.autoconfigure.spi)
testImplementation(projects.sentryAsyncProfiler)
/**
* Adding a version of opentelemetry-spring-boot-starter that doesn't support Spring Boot 4 causes
* java.lang.IllegalArgumentException: Could not find class
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
io.sentry.spring.boot4.SentryAutoConfiguration
io.sentry.spring.boot4.SentryProfilerAutoConfiguration
io.sentry.spring.boot4.SentryLogbackAppenderAutoConfiguration
io.sentry.spring.boot4.SentryWebfluxAutoConfiguration
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ 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
import io.sentry.NoOpContinuousProfiler
import io.sentry.NoOpProfileConverter
import io.sentry.NoOpTransportFactory
import io.sentry.SamplingContext
import io.sentry.Sentry
Expand All @@ -18,6 +22,8 @@ import io.sentry.SentryIntegrationPackageStorage
import io.sentry.SentryLevel
import io.sentry.SentryLogEvent
import io.sentry.SentryOptions
import io.sentry.asyncprofiler.profiling.JavaContinuousProfiler
import io.sentry.asyncprofiler.provider.AsyncProfilerProfileConverterProvider
import io.sentry.checkEvent
import io.sentry.opentelemetry.SentryAutoConfigurationCustomizerProvider
import io.sentry.opentelemetry.agent.AgentMarker
Expand Down Expand Up @@ -45,6 +51,7 @@ import kotlin.test.assertFalse
import kotlin.test.assertTrue
import org.aspectj.lang.ProceedingJoinPoint
import org.assertj.core.api.Assertions.assertThat
import org.mockito.internal.util.MockUtil.isMock
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.mock
Expand Down Expand Up @@ -87,6 +94,7 @@ class SentryAutoConfigurationTest {
AutoConfigurations.of(
SentryAutoConfiguration::class.java,
WebMvcAutoConfiguration::class.java,
SentryProfilerAutoConfiguration::class.java,
)
)

Expand Down Expand Up @@ -1037,6 +1045,94 @@ 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.profile-session-sample-rate=1.0",
)
.run {
assertThat(it).hasSingleBean(IContinuousProfiler::class.java)
assertThat(it).hasSingleBean(IProfileConverter::class.java)
assertThat(it)
.getBean(IProfileConverter::class.java)
.isInstanceOf(
AsyncProfilerProfileConverterProvider.AsyncProfilerProfileConverter::class.java
)
assertThat(it)
.getBean(IContinuousProfiler::class.java)
.isInstanceOf(JavaContinuousProfiler::class.java)
assertThat(it)
.getBean(IProfileConverter::class.java)
.isSameAs(Sentry.getGlobalScope().options.profilerConverter)
assertThat(it)
.getBean(IContinuousProfiler::class.java)
.isSameAs(Sentry.getGlobalScope().options.continuousProfiler)
}
}

@Test
fun `when AgentMarker is on the classpath and ContinuousProfiling is enabled IContinuousProfiler and IProfileConverter exist beans are taken from options`() {
SentryIntegrationPackageStorage.getInstance().clearStorage()

contextRunner
.withPropertyValues(
"sentry.dsn=http://key@localhost/proj",
"sentry.profile-session-sample-rate=1.0",
"sentry.auto-init=false",
"debug=true",
)
.withUserConfiguration(CustomProfilerOptionsConfigurationConfiguration::class.java)
.run {
val profiler = it.getBean(IContinuousProfiler::class.java)
assertTrue(isMock(profiler))
assertThat(it).hasSingleBean(IContinuousProfiler::class.java)
Copy link
Member

Choose a reason for hiding this comment

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

Is this currently simply asserting it has noop instances?

assertThat(it).hasSingleBean(IProfileConverter::class.java)
assertThat(it)
.getBean(IProfileConverter::class.java)
.isSameAs(Sentry.getGlobalScope().options.profilerConverter)
assertThat(it)
.getBean(IContinuousProfiler::class.java)
.isSameAs(Sentry.getGlobalScope().options.continuousProfiler)
}
}

@Test
fun `when AgentMarker is on the classpath and ContinuousProfiling is disabled NoOp Beans are created`() {
SentryIntegrationPackageStorage.getInstance().clearStorage()

contextRunner
.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.auto-init=false")
.run {
assertThat(it).hasSingleBean(IContinuousProfiler::class.java)
assertThat(it).hasSingleBean(IProfileConverter::class.java)
assertThat(it)
.getBean(IProfileConverter::class.java)
.isInstanceOf(NoOpProfileConverter::class.java)
assertThat(it)
.getBean(IContinuousProfiler::class.java)
.isInstanceOf(NoOpContinuousProfiler::class.java)
}
}
Copy link

Choose a reason for hiding this comment

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

Bug: Continuous Profiling Test Lacks Required Sample Rate

The test "when AgentMarker is on the classpath and ContinuousProfiling is enabled..." doesn't actually enable continuous profiling. It's missing the sentry.profile-session-sample-rate=1.0 property, which results in NoOp profiler beans being created instead of functional ones. This makes the test misleading about verifying actual continuous profiling setup, especially since the companion test correctly includes this property.

Additional Locations (2)

Fix in Cursor Fix in Web


@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.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 {
Expand Down Expand Up @@ -1082,6 +1178,17 @@ class SentryAutoConfigurationTest {
@Bean open fun sentryOptionsConfiguration() = Sentry.OptionsConfiguration<SentryOptions> {}
}

@Configuration(proxyBeanMethods = false)
open class CustomProfilerOptionsConfigurationConfiguration {
private val profiler = mock<IContinuousProfiler>()

@Bean
open fun customOptionsConfiguration() =
Sentry.OptionsConfiguration<SentryOptions> { it.setContinuousProfiler(profiler) }

@Bean open fun beforeSendCallback() = CustomBeforeSendCallback()
}

@Configuration(proxyBeanMethods = false)
open class MockTransportConfiguration {

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

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

public class io/sentry/spring/boot/jakarta/SentryProperties : io/sentry/SentryOptions {
public fun <init> ()V
public fun getExceptionResolverOrder ()I
Expand Down
1 change: 1 addition & 0 deletions sentry-spring-boot-jakarta/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ dependencies {
testImplementation(libs.springboot3.starter.test)
testImplementation(libs.springboot3.starter.web)
testImplementation(libs.springboot3.starter.webflux)
testImplementation(projects.sentryAsyncProfiler)
}

configure<SourceSetContainer> { test { java.srcDir("src/test/java") } }
Expand Down
Loading