Skip to content

Commit 3fdc833

Browse files
adinauerclaude
andcommitted
merge(micrometer): Propagate injected scopes to ignored metrics
Merge the Spring Boot registry scopes change from #6116 into #6155. Use the same injected scopes for registration filtering and recording so custom scopes do not accidentally inherit global ignored-metric options. Resolve test imports, supply options in injected-scope fixtures, and cover filtering with distinct global and injected ignore lists. Preserve the existing logging defaults and explicit override behavior. Refs #6116 Refs #6155 Co-Authored-By: Claude <noreply@anthropic.com>
2 parents 20e3db6 + 4b5d105 commit 3fdc833

13 files changed

Lines changed: 204 additions & 18 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/SentryIgnoredMetricsFilter.java‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import io.micrometer.core.instrument.config.MeterFilter;
55
import io.micrometer.core.instrument.config.MeterFilterReply;
66
import io.sentry.FilterString;
7-
import io.sentry.Sentry;
7+
import io.sentry.IScopes;
88
import io.sentry.util.MetricsUtils;
99
import java.util.List;
1010
import java.util.Locale;
@@ -13,15 +13,18 @@
1313

1414
final class SentryIgnoredMetricsFilter implements MeterFilter {
1515
private final @NotNull SentryMeterRegistry registry;
16+
private final @NotNull IScopes scopes;
1617

17-
SentryIgnoredMetricsFilter(final @NotNull SentryMeterRegistry registry) {
18+
SentryIgnoredMetricsFilter(
19+
final @NotNull SentryMeterRegistry registry, final @NotNull IScopes scopes) {
1820
this.registry = registry;
21+
this.scopes = scopes;
1922
}
2023

2124
@Override
2225
public @NotNull MeterFilterReply accept(final @NotNull Meter.Id id) {
2326
final @Nullable List<FilterString> ignoredMetrics =
24-
Sentry.getCurrentScopes().getOptions().getMetrics().getIgnoredMetrics();
27+
scopes.getOptions().getMetrics().getIgnoredMetrics();
2528
if (ignoredMetrics == null || ignoredMetrics.isEmpty()) {
2629
return MeterFilterReply.NEUTRAL;
2730
}

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

Lines changed: 22 additions & 8 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(
@@ -79,7 +93,7 @@ public SentryMeterRegistry(final long pollIntervalMillis) {
7993
this.scheduler = pollIntervalMillis == 0 ? null : scheduler;
8094
config()
8195
.namingConvention(NamingConvention.identity)
82-
.meterFilter(new SentryIgnoredMetricsFilter(this))
96+
.meterFilter(new SentryIgnoredMetricsFilter(this, scopes))
8397
.onMeterRemoved(this::onMeterRemoved);
8498
addIntegrationToSdkVersion(INTEGRATION_NAME);
8599
if (this.scheduler == null) {
@@ -168,7 +182,7 @@ public SentryMeterRegistry(final long pollIntervalMillis) {
168182
final @NotNull Meter.Id id,
169183
final @NotNull Meter.Type type,
170184
final @NotNull Iterable<Measurement> measurements) {
171-
Sentry.getCurrentScopes()
185+
scopes
172186
.getOptions()
173187
.getLogger()
174188
.log(
@@ -192,7 +206,7 @@ void captureCounter(final @NotNull SentryMetricInfo metricInfo, final double val
192206
if (isClosed()) {
193207
return;
194208
}
195-
Sentry.getCurrentScopes()
209+
scopes
196210
.metrics()
197211
.count(metricInfo.getName(), value, metricInfo.getUnit(), metricInfo.createParameters());
198212
}
@@ -201,7 +215,7 @@ void captureDistribution(final @NotNull SentryMetricInfo metricInfo, final doubl
201215
if (isClosed()) {
202216
return;
203217
}
204-
Sentry.getCurrentScopes()
218+
scopes
205219
.metrics()
206220
.distribution(
207221
metricInfo.getName(), value, metricInfo.getUnit(), metricInfo.createParameters());
@@ -211,7 +225,7 @@ void captureGauge(final @NotNull SentryMetricInfo metricInfo, final double value
211225
if (isClosed()) {
212226
return;
213227
}
214-
Sentry.getCurrentScopes()
228+
scopes
215229
.metrics()
216230
.gauge(metricInfo.getName(), value, metricInfo.getUnit(), metricInfo.createParameters());
217231
}
@@ -234,7 +248,7 @@ void pollMeters() {
234248
}
235249

236250
void logPollingFailure(final @NotNull Throwable throwable, final @NotNull String meterName) {
237-
Sentry.getCurrentScopes()
251+
scopes
238252
.getOptions()
239253
.getLogger()
240254
.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/SentryIgnoredMetricsFilterTest.kt‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,36 @@ class SentryIgnoredMetricsFilterTest {
6262
assertThat(events.allValues.map { it.name }).containsExactly("logback.events", "log4j2.events")
6363
}
6464

65+
@Test
66+
fun `registration filtering and recording use injected scopes instead of global scopes`() {
67+
options.metrics.addIgnoredMetric("global[.]blocked")
68+
val injectedOptions =
69+
SentryOptions().apply {
70+
dsn = "https://key@sentry.io/proj"
71+
metrics.addIgnoredMetric("injected[.]blocked")
72+
}
73+
val injectedClient = mock<ISentryClient>()
74+
whenever(injectedClient.isEnabled).thenReturn(true)
75+
val scopes = createTestScopes(injectedOptions)
76+
scopes.bindClient(injectedClient)
77+
78+
val injectedRegistry = SentryMeterRegistry(scopes, 0)
79+
try {
80+
injectedRegistry.counter("injected.blocked").increment()
81+
injectedRegistry.counter("global.blocked").increment(2.0)
82+
83+
assertThat(injectedRegistry.find("injected.blocked").counter()).isNull()
84+
assertThat(injectedRegistry.find("global.blocked").counter()).isNotNull()
85+
val event = argumentCaptor<SentryMetricsEvent>()
86+
verify(injectedClient).captureMetric(event.capture(), any(), anyOrNull())
87+
assertThat(event.firstValue.name).isEqualTo("global.blocked")
88+
assertThat(event.firstValue.value).isEqualTo(2.0)
89+
verify(client, never()).captureMetric(any(), any(), anyOrNull())
90+
} finally {
91+
injectedRegistry.close()
92+
}
93+
}
94+
6595
@Test
6696
fun `denied counter never invokes the metrics API even under a log flood`() {
6797
options.metrics.addIgnoredMetric("LOGBACK.EVENTS")

‎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: 56 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,59 @@ 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+
whenever(scopes.options).thenReturn(SentryOptions())
179+
val registry = SentryMeterRegistry(scopes, 0).also(registries::add)
180+
181+
registry.counter("counter").increment(2.0)
182+
registry.timer("timer").record(3, TimeUnit.MILLISECONDS)
183+
registry.summary("summary").record(4.0)
184+
Gauge.builder("gauge", Supplier { 5.0 }).register(registry)
185+
registry.pollMeters()
186+
187+
verify(metrics).count(eq("counter"), eq(2.0), anyOrNull(), any())
188+
verify(metrics).distribution(eq("timer"), eq(3.0), eq(MetricsUnit.Duration.MILLISECOND), any())
189+
verify(metrics).distribution(eq("summary"), eq(4.0), anyOrNull(), any())
190+
verify(metrics).gauge(eq("gauge"), eq(5.0), anyOrNull(), any())
191+
verifyNoInteractions(globalMetrics)
192+
}
193+
194+
@Test
195+
fun `diagnostic logging uses injected scopes`() {
196+
val logger = mock<ILogger>()
197+
val options =
198+
SentryOptions().apply {
199+
isDebug = true
200+
setLogger(logger)
201+
}
202+
val scopes = mock<IScopes>()
203+
whenever(scopes.options).thenReturn(options)
204+
val registry = SentryMeterRegistry(scopes, 0).also(registries::add)
205+
val failure = IllegalStateException("poll failed")
206+
207+
Meter.builder(
208+
"custom",
209+
Meter.Type.OTHER,
210+
listOf(Measurement(Supplier { 7.0 }, Statistic.VALUE)),
211+
)
212+
.register(registry)
213+
registry.logPollingFailure(failure, "gauge")
214+
215+
verify(logger)
216+
.log(
217+
SentryLevel.DEBUG,
218+
"Micrometer meter type %s is not supported for Sentry export.",
219+
Meter.Type.OTHER,
220+
)
221+
verify(logger)
222+
.log(SentryLevel.DEBUG, failure, "Failed to publish Micrometer meter %s to Sentry.", "gauge")
223+
}
224+
169225
@Test
170226
fun `registry created before Sentry init forwards after initialization`() {
171227
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: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,17 @@ import io.micrometer.core.instrument.simple.SimpleMeterRegistry
66
import io.sentry.IScopes
77
import io.sentry.Sentry
88
import io.sentry.SentryOptions
9+
import io.sentry.metrics.IMetricsApi
910
import io.sentry.micrometer.SentryMeterRegistry
1011
import kotlin.test.AfterTest
1112
import kotlin.test.Test
1213
import org.assertj.core.api.Assertions.assertThat
14+
import org.mockito.kotlin.any
15+
import org.mockito.kotlin.anyOrNull
16+
import org.mockito.kotlin.eq
17+
import org.mockito.kotlin.mock
18+
import org.mockito.kotlin.verify
19+
import org.mockito.kotlin.whenever
1320
import org.springframework.boot.autoconfigure.AutoConfigurations
1421
import org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration
1522
import org.springframework.boot.micrometer.metrics.autoconfigure.MetricsAutoConfiguration
@@ -188,6 +195,25 @@ class SentryMicrometerConfigurationTest {
188195
assertThat(registry!!.isClosed).isTrue()
189196
}
190197

198+
@Test
199+
fun `registry uses the injected scopes bean`() {
200+
val scopes = mock<IScopes>()
201+
val metrics = mock<IMetricsApi>()
202+
whenever(scopes.metrics()).thenReturn(metrics)
203+
val properties = SentryProperties().apply { micrometer.pollIntervalMillis = 0 }
204+
whenever(scopes.options).thenReturn(properties)
205+
206+
ApplicationContextRunner()
207+
.withUserConfiguration(SentryMicrometerConfiguration::class.java)
208+
.withBean(IScopes::class.java, { scopes })
209+
.withBean(SentryProperties::class.java, { properties })
210+
.run {
211+
it.getBean(SentryMeterRegistry::class.java).counter("requests").increment(2.0)
212+
213+
verify(metrics).count(eq("requests"), eq(2.0), anyOrNull(), any())
214+
}
215+
}
216+
191217
@Test
192218
fun `integration is absent when Sentry auto-configuration is disabled`() {
193219
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
}

0 commit comments

Comments
 (0)