Skip to content

Commit 4b5d105

Browse files
adinauerclaude
andcommitted
fix(micrometer): Use injected scopes in the meter registry
Use the supplied IScopes for metric recording and diagnostic logging instead of bypassing it through the static Sentry API. Default standalone constructors to ScopesAdapter so current-context resolution is preserved. Pass the Spring scopes bean through all three Boot integrations while retaining initialization ordering. Cover injected metrics, logging, and Spring wiring with regression tests. Refs #6116 Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 86f7936 commit 4b5d105

11 files changed

Lines changed: 163 additions & 14 deletions

File tree

‎sentry-micrometer/api/sentry-micrometer.api‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ public final class io/sentry/micrometer/BuildConfig {
66
public final class io/sentry/micrometer/SentryMeterRegistry : io/micrometer/core/instrument/MeterRegistry {
77
public fun <init> ()V
88
public fun <init> (J)V
9+
public fun <init> (Lio/sentry/IScopes;J)V
910
public fun close ()V
1011
}
1112

‎sentry-micrometer/src/main/java/io/sentry/micrometer/SentryMeterRegistry.java‎

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@
2222
import io.micrometer.core.instrument.internal.DefaultLongTaskTimer;
2323
import io.micrometer.core.instrument.internal.DefaultMeter;
2424
import io.micrometer.core.instrument.util.NamedThreadFactory;
25-
import io.sentry.Sentry;
25+
import io.sentry.IScopes;
26+
import io.sentry.ScopesAdapter;
2627
import io.sentry.SentryAttributes;
2728
import io.sentry.SentryIntegrationPackageStorage;
2829
import io.sentry.SentryLevel;
2930
import io.sentry.metrics.MetricsUnit;
3031
import io.sentry.util.ExceptionUtils;
32+
import io.sentry.util.Objects;
3133
import java.util.HashMap;
3234
import java.util.Map;
3335
import java.util.concurrent.Executors;
@@ -44,6 +46,7 @@ public final class SentryMeterRegistry extends MeterRegistry {
4446
private static final @NotNull String INTEGRATION_NAME = "Micrometer";
4547
private static final long DEFAULT_POLL_INTERVAL_MILLIS = 60_000;
4648

49+
private final @NotNull IScopes scopes;
4750
private final @Nullable ScheduledExecutorService scheduler;
4851
private final @Nullable ScheduledFuture<?> pollingTask;
4952

@@ -63,14 +66,25 @@ public SentryMeterRegistry() {
6366
* <p>A zero interval disables passive meter polling. Negative intervals are not supported.
6467
*/
6568
public SentryMeterRegistry(final long pollIntervalMillis) {
66-
this(pollIntervalMillis, Clock.SYSTEM, createScheduler(pollIntervalMillis));
69+
this(ScopesAdapter.getInstance(), pollIntervalMillis);
70+
}
71+
72+
/**
73+
* Creates a registry with the given scopes and passive meter polling interval in milliseconds.
74+
*
75+
* <p>A zero interval disables passive meter polling. Negative intervals are not supported.
76+
*/
77+
public SentryMeterRegistry(final @NotNull IScopes scopes, final long pollIntervalMillis) {
78+
this(scopes, pollIntervalMillis, Clock.SYSTEM, createScheduler(pollIntervalMillis));
6779
}
6880

6981
SentryMeterRegistry(
82+
final @NotNull IScopes scopes,
7083
final long pollIntervalMillis,
7184
final @NotNull Clock clock,
7285
final @Nullable ScheduledExecutorService scheduler) {
7386
super(clock);
87+
this.scopes = Objects.requireNonNull(scopes, "Scopes are required");
7488
validatePollInterval(pollIntervalMillis);
7589
if (pollIntervalMillis > 0 && scheduler == null) {
7690
throw new IllegalArgumentException(
@@ -165,7 +179,7 @@ public SentryMeterRegistry(final long pollIntervalMillis) {
165179
final @NotNull Meter.Id id,
166180
final @NotNull Meter.Type type,
167181
final @NotNull Iterable<Measurement> measurements) {
168-
Sentry.getCurrentScopes()
182+
scopes
169183
.getOptions()
170184
.getLogger()
171185
.log(
@@ -189,7 +203,7 @@ void captureCounter(final @NotNull SentryMetricInfo metricInfo, final double val
189203
if (isClosed()) {
190204
return;
191205
}
192-
Sentry.getCurrentScopes()
206+
scopes
193207
.metrics()
194208
.count(metricInfo.getName(), value, metricInfo.getUnit(), metricInfo.createParameters());
195209
}
@@ -198,7 +212,7 @@ void captureDistribution(final @NotNull SentryMetricInfo metricInfo, final doubl
198212
if (isClosed()) {
199213
return;
200214
}
201-
Sentry.getCurrentScopes()
215+
scopes
202216
.metrics()
203217
.distribution(
204218
metricInfo.getName(), value, metricInfo.getUnit(), metricInfo.createParameters());
@@ -208,7 +222,7 @@ void captureGauge(final @NotNull SentryMetricInfo metricInfo, final double value
208222
if (isClosed()) {
209223
return;
210224
}
211-
Sentry.getCurrentScopes()
225+
scopes
212226
.metrics()
213227
.gauge(metricInfo.getName(), value, metricInfo.getUnit(), metricInfo.createParameters());
214228
}
@@ -231,7 +245,7 @@ void pollMeters() {
231245
}
232246

233247
void logPollingFailure(final @NotNull Throwable throwable, final @NotNull String meterName) {
234-
Sentry.getCurrentScopes()
248+
scopes
235249
.getOptions()
236250
.getLogger()
237251
.log(

‎sentry-micrometer/src/test/kotlin/io/sentry/micrometer/SentryFunctionTimerTest.kt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import com.google.common.truth.Truth.assertThat
44
import io.micrometer.core.instrument.Clock
55
import io.micrometer.core.instrument.FunctionTimer
66
import io.sentry.IScopes
7+
import io.sentry.ScopesAdapter
78
import io.sentry.Sentry
89
import io.sentry.SentryOptions
910
import io.sentry.metrics.IMetricsApi
@@ -357,7 +358,8 @@ class SentryFunctionTimerTest {
357358
val scheduler = mock<ScheduledExecutorService>()
358359
val task = mock<ScheduledFuture<Unit>>()
359360
whenever(scheduler.scheduleAtFixedRate(any(), any(), any(), any())).thenReturn(task)
360-
return SentryMeterRegistry(60_000, Clock.SYSTEM, scheduler).also(registries::add)
361+
return SentryMeterRegistry(ScopesAdapter.getInstance(), 60_000, Clock.SYSTEM, scheduler)
362+
.also(registries::add)
361363
}
362364

363365
private fun installMetricsApi(): IMetricsApi {

‎sentry-micrometer/src/test/kotlin/io/sentry/micrometer/SentryMeterRegistryPollingTest.kt‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import io.micrometer.core.instrument.LongTaskTimer
88
import io.micrometer.core.instrument.MockClock
99
import io.micrometer.core.instrument.TimeGauge
1010
import io.sentry.IScopes
11+
import io.sentry.ScopesAdapter
1112
import io.sentry.Sentry
1213
import io.sentry.SentryOptions
1314
import io.sentry.metrics.IMetricsApi
@@ -66,7 +67,8 @@ class SentryMeterRegistryPollingTest {
6667
)
6768
.thenReturn(task)
6869

69-
val registry = track(SentryMeterRegistry(2500, Clock.SYSTEM, scheduler))
70+
val registry =
71+
track(SentryMeterRegistry(ScopesAdapter.getInstance(), 2500, Clock.SYSTEM, scheduler))
7072
val value = AtomicReference(4.5)
7173
Gauge.builder("scheduled", value) { it.get() }.strongReference(true).register(registry)
7274
val scheduledPoll = argumentCaptor<Runnable>()
@@ -107,7 +109,7 @@ class SentryMeterRegistryPollingTest {
107109
@Test
108110
fun `zero interval ignores an available scheduler`() {
109111
val scheduler = mock<ScheduledExecutorService>()
110-
track(SentryMeterRegistry(0, Clock.SYSTEM, scheduler))
112+
track(SentryMeterRegistry(ScopesAdapter.getInstance(), 0, Clock.SYSTEM, scheduler))
111113

112114
verifyNoInteractions(scheduler)
113115
}
@@ -406,7 +408,7 @@ class SentryMeterRegistryPollingTest {
406408
val scheduler = mock<ScheduledExecutorService>()
407409
val task = mock<ScheduledFuture<Unit>>()
408410
whenever(scheduler.scheduleAtFixedRate(any(), any(), any(), any())).thenReturn(task)
409-
return track(SentryMeterRegistry(60_000, clock, scheduler))
411+
return track(SentryMeterRegistry(ScopesAdapter.getInstance(), 60_000, clock, scheduler))
410412
}
411413

412414
private fun track(registry: SentryMeterRegistry): SentryMeterRegistry {

‎sentry-micrometer/src/test/kotlin/io/sentry/micrometer/SentryMeterRegistryTest.kt‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package io.sentry.micrometer
33
import com.google.common.truth.Truth.assertThat
44
import io.micrometer.core.instrument.Counter
55
import io.micrometer.core.instrument.DistributionSummary
6+
import io.micrometer.core.instrument.Gauge
67
import io.micrometer.core.instrument.Measurement
78
import io.micrometer.core.instrument.Meter
89
import io.micrometer.core.instrument.Statistic
@@ -12,10 +13,12 @@ import io.micrometer.core.instrument.config.MeterFilter
1213
import io.micrometer.core.instrument.config.NamingConvention
1314
import io.micrometer.core.instrument.simple.SimpleMeterRegistry
1415
import io.sentry.Hint
16+
import io.sentry.ILogger
1517
import io.sentry.IScopes
1618
import io.sentry.ISentryClient
1719
import io.sentry.Sentry
1820
import io.sentry.SentryIntegrationPackageStorage
21+
import io.sentry.SentryLevel
1922
import io.sentry.SentryMetricsEvent
2023
import io.sentry.SentryOptions
2124
import io.sentry.metrics.IMetricsApi
@@ -166,6 +169,58 @@ class SentryMeterRegistryTest {
166169
verify(second).count(eq("counter"), eq(2.0), anyOrNull(), any())
167170
}
168171

172+
@Test
173+
fun `injected scopes receive active and passive metrics instead of global scopes`() {
174+
val globalMetrics = installMetricsApi()
175+
val metrics = mock<IMetricsApi>()
176+
val scopes = mock<IScopes>()
177+
whenever(scopes.metrics()).thenReturn(metrics)
178+
val registry = SentryMeterRegistry(scopes, 0).also(registries::add)
179+
180+
registry.counter("counter").increment(2.0)
181+
registry.timer("timer").record(3, TimeUnit.MILLISECONDS)
182+
registry.summary("summary").record(4.0)
183+
Gauge.builder("gauge", Supplier { 5.0 }).register(registry)
184+
registry.pollMeters()
185+
186+
verify(metrics).count(eq("counter"), eq(2.0), anyOrNull(), any())
187+
verify(metrics).distribution(eq("timer"), eq(3.0), eq(MetricsUnit.Duration.MILLISECOND), any())
188+
verify(metrics).distribution(eq("summary"), eq(4.0), anyOrNull(), any())
189+
verify(metrics).gauge(eq("gauge"), eq(5.0), anyOrNull(), any())
190+
verifyNoInteractions(globalMetrics)
191+
}
192+
193+
@Test
194+
fun `diagnostic logging uses injected scopes`() {
195+
val logger = mock<ILogger>()
196+
val options =
197+
SentryOptions().apply {
198+
isDebug = true
199+
setLogger(logger)
200+
}
201+
val scopes = mock<IScopes>()
202+
whenever(scopes.options).thenReturn(options)
203+
val registry = SentryMeterRegistry(scopes, 0).also(registries::add)
204+
val failure = IllegalStateException("poll failed")
205+
206+
Meter.builder(
207+
"custom",
208+
Meter.Type.OTHER,
209+
listOf(Measurement(Supplier { 7.0 }, Statistic.VALUE)),
210+
)
211+
.register(registry)
212+
registry.logPollingFailure(failure, "gauge")
213+
214+
verify(logger)
215+
.log(
216+
SentryLevel.DEBUG,
217+
"Micrometer meter type %s is not supported for Sentry export.",
218+
Meter.Type.OTHER,
219+
)
220+
verify(logger)
221+
.log(SentryLevel.DEBUG, failure, "Failed to publish Micrometer meter %s to Sentry.", "gauge")
222+
}
223+
169224
@Test
170225
fun `registry created before Sentry init forwards after initialization`() {
171226
Sentry.close()

‎sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryMicrometerConfiguration.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ final class SentryMicrometerConfiguration {
1515
@ConditionalOnMissingBean(SentryMeterRegistry.class)
1616
public @NotNull SentryMeterRegistry sentryMeterRegistry(
1717
final @NotNull IScopes scopes, final @NotNull SentryProperties properties) {
18-
return new SentryMeterRegistry(properties.getMicrometer().getPollIntervalMillis());
18+
return new SentryMeterRegistry(scopes, properties.getMicrometer().getPollIntervalMillis());
1919
}
2020
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,17 @@ import io.micrometer.core.instrument.composite.CompositeMeterRegistry
55
import io.micrometer.core.instrument.simple.SimpleMeterRegistry
66
import io.sentry.IScopes
77
import io.sentry.Sentry
8+
import io.sentry.metrics.IMetricsApi
89
import io.sentry.micrometer.SentryMeterRegistry
910
import kotlin.test.AfterTest
1011
import kotlin.test.Test
1112
import org.assertj.core.api.Assertions.assertThat
13+
import org.mockito.kotlin.any
14+
import org.mockito.kotlin.anyOrNull
15+
import org.mockito.kotlin.eq
16+
import org.mockito.kotlin.mock
17+
import org.mockito.kotlin.verify
18+
import org.mockito.kotlin.whenever
1219
import org.springframework.boot.autoconfigure.AutoConfigurations
1320
import org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration
1421
import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration
@@ -72,6 +79,24 @@ class SentryMicrometerConfigurationTest {
7279
assertThat(registry!!.isClosed).isTrue()
7380
}
7481

82+
@Test
83+
fun `registry uses the injected scopes bean`() {
84+
val scopes = mock<IScopes>()
85+
val metrics = mock<IMetricsApi>()
86+
whenever(scopes.metrics()).thenReturn(metrics)
87+
val properties = SentryProperties().apply { micrometer.pollIntervalMillis = 0 }
88+
89+
ApplicationContextRunner()
90+
.withUserConfiguration(SentryMicrometerConfiguration::class.java)
91+
.withBean(IScopes::class.java, { scopes })
92+
.withBean(SentryProperties::class.java, { properties })
93+
.run {
94+
it.getBean(SentryMeterRegistry::class.java).counter("requests").increment(2.0)
95+
96+
verify(metrics).count(eq("requests"), eq(2.0), anyOrNull(), any())
97+
}
98+
}
99+
75100
@Test
76101
fun `integration is absent when Sentry auto-configuration is disabled`() {
77102
ApplicationContextRunner()

‎sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryMicrometerConfiguration.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ final class SentryMicrometerConfiguration {
1515
@ConditionalOnMissingBean(SentryMeterRegistry.class)
1616
public @NotNull SentryMeterRegistry sentryMeterRegistry(
1717
final @NotNull IScopes scopes, final @NotNull SentryProperties properties) {
18-
return new SentryMeterRegistry(properties.getMicrometer().getPollIntervalMillis());
18+
return new SentryMeterRegistry(scopes, properties.getMicrometer().getPollIntervalMillis());
1919
}
2020
}

‎sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryMicrometerConfigurationTest.kt‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,17 @@ import io.micrometer.core.instrument.composite.CompositeMeterRegistry
55
import io.micrometer.core.instrument.simple.SimpleMeterRegistry
66
import io.sentry.IScopes
77
import io.sentry.Sentry
8+
import io.sentry.metrics.IMetricsApi
89
import io.sentry.micrometer.SentryMeterRegistry
910
import kotlin.test.AfterTest
1011
import kotlin.test.Test
1112
import org.assertj.core.api.Assertions.assertThat
13+
import org.mockito.kotlin.any
14+
import org.mockito.kotlin.anyOrNull
15+
import org.mockito.kotlin.eq
16+
import org.mockito.kotlin.mock
17+
import org.mockito.kotlin.verify
18+
import org.mockito.kotlin.whenever
1219
import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration
1320
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration
1421
import org.springframework.boot.autoconfigure.AutoConfigurations
@@ -72,6 +79,24 @@ class SentryMicrometerConfigurationTest {
7279
assertThat(registry!!.isClosed).isTrue()
7380
}
7481

82+
@Test
83+
fun `registry uses the injected scopes bean`() {
84+
val scopes = mock<IScopes>()
85+
val metrics = mock<IMetricsApi>()
86+
whenever(scopes.metrics()).thenReturn(metrics)
87+
val properties = SentryProperties().apply { micrometer.pollIntervalMillis = 0 }
88+
89+
ApplicationContextRunner()
90+
.withUserConfiguration(SentryMicrometerConfiguration::class.java)
91+
.withBean(IScopes::class.java, { scopes })
92+
.withBean(SentryProperties::class.java, { properties })
93+
.run {
94+
it.getBean(SentryMeterRegistry::class.java).counter("requests").increment(2.0)
95+
96+
verify(metrics).count(eq("requests"), eq(2.0), anyOrNull(), any())
97+
}
98+
}
99+
75100
@Test
76101
fun `integration is absent when Sentry auto-configuration is disabled`() {
77102
ApplicationContextRunner()

‎sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryMicrometerConfiguration.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ final class SentryMicrometerConfiguration {
1515
@ConditionalOnMissingBean(SentryMeterRegistry.class)
1616
public @NotNull SentryMeterRegistry sentryMeterRegistry(
1717
final @NotNull IScopes scopes, final @NotNull SentryProperties properties) {
18-
return new SentryMeterRegistry(properties.getMicrometer().getPollIntervalMillis());
18+
return new SentryMeterRegistry(scopes, properties.getMicrometer().getPollIntervalMillis());
1919
}
2020
}

0 commit comments

Comments
 (0)