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,73 @@
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.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)
}
}
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
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,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
Expand Down Expand Up @@ -87,6 +89,7 @@ class SentryAutoConfigurationTest {
AutoConfigurations.of(
SentryAutoConfiguration::class.java,
WebMvcAutoConfiguration::class.java,
SentryProfilerAutoConfiguration::class.java,
)
)

Expand Down Expand Up @@ -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",
Copy link
Member

Choose a reason for hiding this comment

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

Should this also have "sentry.profile-session-sample-rate=1.0",?

"sentry.auto-init=false",
"debug=true",
)
.run {
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)
}
}
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.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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ public class io/sentry/spring/boot/jakarta/SentryLogbackInitializer : org/spring
public fun supportsEventType (Lorg/springframework/core/ResolvableType;)Z
}

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

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

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

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

@lbloder lbloder Oct 20, 2025

Choose a reason for hiding this comment

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

Make it conditional on the async profiler class too?

Copy link
Member

Choose a reason for hiding this comment

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

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

@Open
@Import(SentryProfilerConfiguration.class)
public class SentryProfilerAutoConfiguration {}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
io.sentry.spring.boot.jakarta.SentryAutoConfiguration
io.sentry.spring.boot.jakarta.SentryProfilerAutoConfiguration
io.sentry.spring.boot.jakarta.SentryLogbackAppenderAutoConfiguration
io.sentry.spring.boot.jakarta.SentryWebfluxAutoConfiguration
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -91,6 +93,7 @@ class SentryAutoConfigurationTest {
AutoConfigurations.of(
SentryAutoConfiguration::class.java,
WebMvcAutoConfiguration::class.java,
SentryProfilerAutoConfiguration::class.java,
)
)

Expand Down Expand Up @@ -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)
}
}
Copy link

Choose a reason for hiding this comment

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

Bug: Missing profile-session sample rate in tests

The tests asserting continuous profiling is enabled are missing the sentry.profile-session-sample-rate=1.0 property. This means continuous profiling isn't actually active during these tests, which contradicts their names and intent.

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

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

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 {}
Original file line number Diff line number Diff line change
@@ -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

Expand Down
Loading
Loading