From 646892e1cfc0a883bcf2aa5e9841e82dde9d92ec Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 23 Sep 2026 14:34:16 +0200 Subject: [PATCH 1/3] feat(metrics): [Micrometer 5] Add ignored metric names Let applications suppress noisy metrics before event processing using case-insensitive names or full regular expressions. Support external configuration, Android manifest metadata, and Spring Boot properties. Default Spring Boot applications to excluding Logback and Log4j2 counters to prevent logging-driven queue overload. Preserve explicit lists, including empty lists, and allow user callbacks to append, replace, or clear defaults. Keep plain Java and Android defaults unfiltered. Reuse the core matcher in a Sentry-local Micrometer MeterFilter to avoid registration and recording overhead without affecting other registries. Preserve partial exports for meters with multiple possible output names. Cover configuration, matching, name conversion, and filtering behavior with tests. Verify that filtering logging counters prevents queue overflow and preserves business metrics during a 100,000-log-per-second workload. Co-Authored-By: Codex --- CHANGELOG.md | 21 ++ .../android/core/ManifestMetadataReader.java | 5 + .../core/ManifestMetadataReaderTest.kt | 42 +++ sentry-micrometer/README.md | 64 +++- .../SentryIgnoredMetricsFilter.java | 66 ++++ .../micrometer/SentryMeterRegistry.java | 5 +- .../SentryIgnoredMetricsFilterTest.kt | 290 ++++++++++++++++++ .../spring/boot4/SentryAutoConfiguration.java | 5 + .../boot4/SentryAutoConfigurationTest.kt | 3 + .../SentryMicrometerConfigurationTest.kt | 121 ++++++++ .../boot/jakarta/SentryAutoConfiguration.java | 5 + .../jakarta/SentryAutoConfigurationTest.kt | 3 + .../SentryMicrometerConfigurationTest.kt | 121 ++++++++ .../spring/boot/SentryAutoConfiguration.java | 5 + .../boot/SentryAutoConfigurationTest.kt | 3 + .../boot/SentryMicrometerConfigurationTest.kt | 121 ++++++++ sentry/api/sentry.api | 9 + .../main/java/io/sentry/ExternalOptions.java | 10 + .../main/java/io/sentry/SentryOptions.java | 50 +++ .../java/io/sentry/metrics/MetricsApi.java | 3 +- .../java/io/sentry/util/MetricsUtils.java | 30 ++ .../java/io/sentry/ExternalOptionsTest.kt | 23 ++ sentry/src/test/java/io/sentry/ScopesTest.kt | 47 +++ .../test/java/io/sentry/SentryOptionsTest.kt | 43 +++ .../java/io/sentry/util/MetricsUtilsTest.kt | 48 +++ 25 files changed, 1135 insertions(+), 8 deletions(-) create mode 100644 sentry-micrometer/src/main/java/io/sentry/micrometer/SentryIgnoredMetricsFilter.java create mode 100644 sentry-micrometer/src/test/kotlin/io/sentry/micrometer/SentryIgnoredMetricsFilterTest.kt create mode 100644 sentry/src/main/java/io/sentry/util/MetricsUtils.java create mode 100644 sentry/src/test/java/io/sentry/util/MetricsUtilsTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 827928cd49..a946415fdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,27 @@ ### Features - Add a Micrometer metrics integration with opt-in Spring Boot support ([#6116](https://github.com/getsentry/sentry-java/pull/6116)) +- Add `options.getMetrics().setIgnoredMetrics(...)` to filter metric names before processing, including early Micrometer filtering + - Configure names or full regular-expression patterns through the Java API, Android manifest, `sentry.properties`, or Spring Boot's `application.properties`. Use `[.]` to match a literal dot in a pattern. + - **Spring Boot 2, 3, and 4 default:** ignore `logback.events` and `log4j2.events` metrics to avoid logging-driven metric queue overload. An explicit list replaces these defaults; an empty list disables filtering. Outside Spring Boot, no metric names are ignored by default. Actual log messages are unaffected. + - **AndroidManifest.xml:** add `io.sentry.metrics.ignored-metrics` under ``: + ```xml + + ``` + - **sentry.properties:** use `metrics.ignored-metrics` (requires external configuration to be enabled with `options.setEnableExternalConfiguration(true)`): + ```properties + metrics.ignored-metrics=noisy[.]metric + ``` + - **Spring Boot application.properties:** use `sentry.metrics.ignored-metrics`. Include the logging patterns to retain the defaults while adding another exclusion: + ```properties + sentry.metrics.ignored-metrics=logback[.]events,log4j2[.]events,noisy[.]metric + ``` + To disable all name filters, including the Spring Boot defaults: + ```properties + sentry.metrics.ignored-metrics= + ``` ## 8.56.0 diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index 1ca91bbada..6a33c503a5 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -168,6 +168,7 @@ final class ManifestMetadataReader { static final String ENABLE_LOGS = "io.sentry.logs.enabled"; static final String ENABLE_METRICS = "io.sentry.metrics.enabled"; + static final String IGNORED_METRICS = "io.sentry.metrics.ignored-metrics"; static final String ENABLE_AUTO_TRACE_ID_GENERATION = "io.sentry.traces.enable-auto-id-generation"; @@ -717,6 +718,10 @@ static void applyMetadata( .getMetrics() .setEnabled( readBool(metadata, logger, ENABLE_METRICS, options.getMetrics().isEnabled())); + final @Nullable List ignoredMetrics = readList(metadata, logger, IGNORED_METRICS); + if (ignoredMetrics != null) { + options.getMetrics().setIgnoredMetrics(ignoredMetrics); + } final @NotNull SentryFeedbackOptions feedbackOptions = options.getFeedbackOptions(); feedbackOptions.setNameRequired( diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt index c387cc8794..3fa43d120e 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt @@ -2016,6 +2016,48 @@ class ManifestMetadataReaderTest { assertTrue(fixture.options.logs.isEnabled) } + @Test + fun `applyMetadata reads ignored metrics`() { + val context = + fixture.getContext( + metaData = bundleOf(ManifestMetadataReader.IGNORED_METRICS to "logback.events,jvm[.].*") + ) + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + assertThat(fixture.options.metrics.ignoredMetrics) + .containsExactly(FilterString("logback.events"), FilterString("jvm[.].*")) + } + + @Test + fun `applyMetadata does not ignore any metrics by default`() { + ManifestMetadataReader.applyMetadata( + fixture.getContext(), + fixture.options, + fixture.buildInfoProvider, + ) + assertThat(fixture.options.metrics.ignoredMetrics).isNull() + } + + @Test + fun `absent manifest ignored metrics preserve configured filters`() { + fixture.options.metrics.addIgnoredMetric("logback.events") + ManifestMetadataReader.applyMetadata( + fixture.getContext(), + fixture.options, + fixture.buildInfoProvider, + ) + assertThat(fixture.options.metrics.ignoredMetrics) + .containsExactly(FilterString("logback.events")) + } + + @Test + fun `empty manifest ignored metrics clear configured filters`() { + fixture.options.metrics.addIgnoredMetric("logback.events") + val context = + fixture.getContext(metaData = bundleOf(ManifestMetadataReader.IGNORED_METRICS to "")) + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + assertThat(fixture.options.metrics.ignoredMetrics).isEmpty() + } + @Test fun `applyMetadata reads metrics enabled and keep default value if not found`() { // Arrange diff --git a/sentry-micrometer/README.md b/sentry-micrometer/README.md index 7712c0bf19..b8d1eb122a 100644 --- a/sentry-micrometer/README.md +++ b/sentry-micrometer/README.md @@ -48,9 +48,10 @@ Auto-configuration requires Sentry to be initialized and backs off when the appl its own `SentryMeterRegistry` bean. Spring Boot adds the registry to its primary composite registry, applies compatible `MeterRegistryCustomizer` beans, and closes it with the application context. Supported metrics registered automatically by Spring Boot Actuator—including HTTP server, JVM, -process, and logging metrics—are forwarded through the same registry. Set the polling interval to -zero to keep immediate forwarding enabled without a polling worker; passive auto-generated meters -then remain registered but are not sent. +and process metrics—are forwarded through the same registry. Sentry's Spring Boot auto-configuration +ignores Logback and Log4j2 logging counters by default; see [Filtering and volume](#filtering-and-volume). +Set the polling interval to zero to keep immediate forwarding enabled without a polling worker; +passive auto-generated meters then remain registered but are not sent. ## Metric mappings @@ -108,9 +109,60 @@ context that changed a backing value. ## Filtering and volume -Each active timer or distribution-summary recording creates one Sentry metric before the existing -Sentry metrics batch processor batches it for transport. Apply Micrometer `MeterFilter`s directly -to the Sentry registry to control volume and cardinality without affecting other registries: +Each counter increment, timer recording, or distribution-summary recording creates one Sentry +metric before the metrics batch processor batches it for transport. This includes each log event +counted by Micrometer's Logback or Log4j2 binders. High logging volume can fill the shared metrics +queue and cause other metrics to be dropped. + +Outside Spring Boot, no metric names are ignored by default. Configure ignored names before +registering meters: + +```java +options.getMetrics().setIgnoredMetrics(Arrays.asList("logback[.]events", "log4j2[.]events")); +``` + +Sentry's Spring Boot 2, 3, and 4 auto-configuration defaults to `logback[.]events` and +`log4j2[.]events` when the ignored-metrics list is unset. These patterns match the logging counters +`logback.events` and `log4j2.events` without treating the dots as regex wildcards. This filters only +metrics, not actual log messages, Sentry Logs, breadcrumbs, or error events. + +An explicit list replaces these defaults. Include them to retain logging exclusions alongside +custom filters: + +```properties +sentry.metrics.ignored-metrics=logback[.]events,log4j2[.]events,my.noisy.metric +``` + +To disable all name filters, including the logging defaults: + +```properties +sentry.metrics.ignored-metrics= +``` + +Defaults are applied before `Sentry.OptionsConfiguration` callbacks. A callback can append filters +with `options.getMetrics().addIgnoredMetric(...)`, replace them with `setIgnoredMetrics(...)`, or +clear them with an empty list or `null`. Enabled external configuration is merged afterward. +These Boot defaults apply even when Micrometer export is disabled, so manually recorded Sentry +metrics with the same names are also filtered. + +For `sentry.properties`, use `metrics.ignored-metrics`; the environment variable is +`SENTRY_METRICS_IGNORED_METRICS`. Android supports the manifest metadata key +`io.sentry.metrics.ignored-metrics` with a comma-separated string value. + +Patterns match final exported Sentry names after Micrometer naming conventions, using +case-insensitive exact matches or full regular-expression matches. Derived names such as +`task.active`, `task.duration`, `task.count`, and `task.total_time` are matched individually. +The option applies to manual metrics too, but does not affect other Micrometer registries. +All Micrometer metrics share `auto.metrics.micrometer` as their origin, so origin cannot +select just logging metrics. + +Sentry checks ignored names before creating metric events. The registry also denies registration +when all possible exported names of a meter are ignored, avoiding recording and polling overhead. +Changing the list later still filters captured metrics, but does not remove existing meters or +reactivate previously returned no-op meters. Configure filters before registration for the lowest +overhead. Intentional ignores do not generate client reports. + +Apply Micrometer `MeterFilter`s directly to the Sentry registry for Micrometer-only filtering: ```java sentryRegistry.config().meterFilter(MeterFilter.denyNameStartsWith("jvm.buffer")); diff --git a/sentry-micrometer/src/main/java/io/sentry/micrometer/SentryIgnoredMetricsFilter.java b/sentry-micrometer/src/main/java/io/sentry/micrometer/SentryIgnoredMetricsFilter.java new file mode 100644 index 0000000000..df32f033e9 --- /dev/null +++ b/sentry-micrometer/src/main/java/io/sentry/micrometer/SentryIgnoredMetricsFilter.java @@ -0,0 +1,66 @@ +package io.sentry.micrometer; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.config.MeterFilter; +import io.micrometer.core.instrument.config.MeterFilterReply; +import io.sentry.FilterString; +import io.sentry.Sentry; +import io.sentry.util.MetricsUtils; +import java.util.List; +import java.util.Locale; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +final class SentryIgnoredMetricsFilter implements MeterFilter { + private final @NotNull SentryMeterRegistry registry; + + SentryIgnoredMetricsFilter(final @NotNull SentryMeterRegistry registry) { + this.registry = registry; + } + + @Override + public @NotNull MeterFilterReply accept(final @NotNull Meter.Id id) { + final @Nullable List ignoredMetrics = + Sentry.getCurrentScopes().getOptions().getMetrics().getIgnoredMetrics(); + if (ignoredMetrics == null || ignoredMetrics.isEmpty()) { + return MeterFilterReply.NEUTRAL; + } + final boolean ignored; + switch (id.getType()) { + case LONG_TASK_TIMER: + final String longTaskName = getTimeMeterName(id); + ignored = + MetricsUtils.isIgnored(ignoredMetrics, longTaskName + ".active") + && MetricsUtils.isIgnored(ignoredMetrics, longTaskName + ".duration"); + break; + case TIMER: + // Timer and FunctionTimer share a type. Keep the meter unless all possible outputs + // are ignored; MetricsApi filters each individual output when it is captured. + final String timerName = getTimeMeterName(id); + ignored = + MetricsUtils.isIgnored(ignoredMetrics, timerName) + && MetricsUtils.isIgnored(ignoredMetrics, timerName + ".count") + && MetricsUtils.isIgnored(ignoredMetrics, timerName + ".total_time"); + break; + case GAUGE: + // Gauge and TimeGauge also share a type, but only TimeGauge changes the base unit. + ignored = + MetricsUtils.isIgnored( + ignoredMetrics, id.getConventionName(registry.config().namingConvention())) + && MetricsUtils.isIgnored(ignoredMetrics, getTimeMeterName(id)); + break; + default: + ignored = + MetricsUtils.isIgnored( + ignoredMetrics, id.getConventionName(registry.config().namingConvention())); + break; + } + return ignored ? MeterFilterReply.DENY : MeterFilterReply.NEUTRAL; + } + + private @NotNull String getTimeMeterName(final @NotNull Meter.Id id) { + // MeterRegistry normalizes time-meter IDs after its filters run, using the default locale. + return id.withBaseUnit(registry.getBaseTimeUnit().toString().toLowerCase(Locale.getDefault())) + .getConventionName(registry.config().namingConvention()); + } +} diff --git a/sentry-micrometer/src/main/java/io/sentry/micrometer/SentryMeterRegistry.java b/sentry-micrometer/src/main/java/io/sentry/micrometer/SentryMeterRegistry.java index e816713ada..8def786d18 100644 --- a/sentry-micrometer/src/main/java/io/sentry/micrometer/SentryMeterRegistry.java +++ b/sentry-micrometer/src/main/java/io/sentry/micrometer/SentryMeterRegistry.java @@ -77,7 +77,10 @@ public SentryMeterRegistry(final long pollIntervalMillis) { "A scheduler is required when passive polling is enabled."); } this.scheduler = pollIntervalMillis == 0 ? null : scheduler; - config().namingConvention(NamingConvention.identity).onMeterRemoved(this::onMeterRemoved); + config() + .namingConvention(NamingConvention.identity) + .meterFilter(new SentryIgnoredMetricsFilter(this)) + .onMeterRemoved(this::onMeterRemoved); addIntegrationToSdkVersion(INTEGRATION_NAME); if (this.scheduler == null) { pollingTask = null; diff --git a/sentry-micrometer/src/test/kotlin/io/sentry/micrometer/SentryIgnoredMetricsFilterTest.kt b/sentry-micrometer/src/test/kotlin/io/sentry/micrometer/SentryIgnoredMetricsFilterTest.kt new file mode 100644 index 0000000000..232bae3e04 --- /dev/null +++ b/sentry-micrometer/src/test/kotlin/io/sentry/micrometer/SentryIgnoredMetricsFilterTest.kt @@ -0,0 +1,290 @@ +package io.sentry.micrometer + +import com.google.common.truth.Truth.assertThat +import io.micrometer.core.instrument.FunctionCounter +import io.micrometer.core.instrument.FunctionTimer +import io.micrometer.core.instrument.Gauge +import io.micrometer.core.instrument.LongTaskTimer +import io.micrometer.core.instrument.Meter +import io.micrometer.core.instrument.TimeGauge +import io.micrometer.core.instrument.composite.CompositeMeterRegistry +import io.micrometer.core.instrument.config.MeterFilter +import io.micrometer.core.instrument.config.NamingConvention +import io.micrometer.core.instrument.simple.SimpleMeterRegistry +import io.sentry.IScopes +import io.sentry.ISentryClient +import io.sentry.Sentry +import io.sentry.SentryMetricsEvent +import io.sentry.SentryOptions +import io.sentry.metrics.IMetricsApi +import io.sentry.test.createTestScopes +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever + +class SentryIgnoredMetricsFilterTest { + private val options = SentryOptions().apply { dsn = "https://key@sentry.io/proj" } + private val client = mock() + private lateinit var registry: SentryMeterRegistry + + @BeforeTest + fun setUp() { + whenever(client.isEnabled).thenReturn(true) + val scopes = createTestScopes(options) + scopes.bindClient(client) + Sentry.setCurrentScopes(scopes) + registry = SentryMeterRegistry(0) + } + + @AfterTest + fun tearDown() { + registry.close() + Sentry.close() + } + + @Test + fun `logging metrics are not ignored by default`() { + registry.counter("logback.events").increment() + registry.counter("log4j2.events").increment() + val events = argumentCaptor() + verify(client, times(2)).captureMetric(events.capture(), any(), anyOrNull()) + assertThat(events.allValues.map { it.name }).containsExactly("logback.events", "log4j2.events") + } + + @Test + fun `denied counter never invokes the metrics API even under a log flood`() { + options.metrics.addIgnoredMetric("LOGBACK.EVENTS") + val api = mock() + val scopes = mock() + whenever(scopes.options).thenReturn(options) + whenever(scopes.metrics()).thenReturn(api) + Sentry.setCurrentScopes(scopes) + + val counter = registry.counter("logback.events") + repeat(100_000) { counter.increment() } + + assertThat(registry.meters).isEmpty() + verifyNoInteractions(api) + } + + @Test + fun `ignored active and passive meters are not registered or evaluated`() { + options.metrics.addIgnoredMetric("ignored[.].*") + val reads = AtomicInteger() + val state = AtomicInteger() + registry.counter("ignored.counter").increment() + registry.timer("ignored.timer").record(1, TimeUnit.SECONDS) + registry.summary("ignored.summary").record(1.0) + Gauge.builder("ignored.gauge", state) { reads.incrementAndGet().toDouble() }.register(registry) + TimeGauge.builder("ignored.time", state, TimeUnit.SECONDS) { + reads.incrementAndGet().toDouble() + } + .register(registry) + FunctionCounter.builder("ignored.function", state) { reads.incrementAndGet().toDouble() } + .register(registry) + FunctionTimer.builder( + "ignored.function.timer", + state, + { reads.incrementAndGet().toLong() }, + { reads.incrementAndGet().toDouble() }, + TimeUnit.SECONDS, + ) + .register(registry) + LongTaskTimer.builder("ignored.long.timer").register(registry).start().stop() + registry.pollMeters() + + assertThat(registry.meters).isEmpty() + assertThat(reads.get()).isEqualTo(0) + verify(client, never()).captureMetric(any(), any(), anyOrNull()) + } + + @Test + fun `filter matches the final mapped and convention-converted name`() { + options.metrics.addIgnoredMetric("export_logback_events") + registry.config().namingConvention(NamingConvention.snakeCase) + registry + .config() + .meterFilter( + object : MeterFilter { + override fun map(id: Meter.Id): Meter.Id = id.withName("export." + id.name) + } + ) + + registry.counter("logback.events").increment() + + assertThat(registry.meters).isEmpty() + verify(client, never()).captureMetric(any(), any(), anyOrNull()) + } + + @Test + fun `filter does not match the raw name when convention changes the export`() { + options.metrics.addIgnoredMetric("logback.events") + registry + .config() + .namingConvention( + object : NamingConvention { + override fun name(name: String, type: Meter.Type, baseUnit: String?) = "export_$name" + } + ) + registry.counter("logback.events").increment() + val event = argumentCaptor() + verify(client).captureMetric(event.capture(), any(), anyOrNull()) + assertThat(event.firstValue.name).isEqualTo("export_logback.events") + } + + @Test + fun `ignoring a timer base name preserves function timer outputs`() { + options.metrics.addIgnoredMetric("task") + val state = AtomicInteger() + FunctionTimer.builder( + "task", + state, + { it.get().toLong() }, + { it.get().toDouble() }, + TimeUnit.SECONDS, + ) + .register(registry) + registry.pollMeters() + state.set(2) + registry.pollMeters() + val events = argumentCaptor() + verify(client, times(2)).captureMetric(events.capture(), any(), anyOrNull()) + assertThat(events.allValues.map { it.name }).containsExactly("task.count", "task.total_time") + } + + @Test + fun `ignoring function timer suffixes preserves ordinary timers`() { + options.metrics.setIgnoredMetrics(listOf("task.count", "task.total_time")) + registry.timer("task").record(1, TimeUnit.SECONDS) + val event = argumentCaptor() + verify(client).captureMetric(event.capture(), any(), anyOrNull()) + assertThat(event.firstValue.name).isEqualTo("task") + } + + @Test + fun `core filter drops only the ignored function timer output`() { + options.metrics.addIgnoredMetric("task.count") + val state = AtomicInteger() + FunctionTimer.builder( + "task", + state, + { it.get().toLong() }, + { it.get().toDouble() }, + TimeUnit.SECONDS, + ) + .register(registry) + registry.pollMeters() + state.set(2) + registry.pollMeters() + val event = argumentCaptor() + verify(client).captureMetric(event.capture(), any(), anyOrNull()) + assertThat(event.firstValue.name).isEqualTo("task.total_time") + assertThat(event.firstValue.value).isEqualTo(2000.0) + } + + @Test + fun `core filter drops only the ignored long task timer output`() { + options.metrics.addIgnoredMetric("task.active") + LongTaskTimer.builder("task").register(registry).start() + registry.pollMeters() + val event = argumentCaptor() + verify(client).captureMetric(event.capture(), any(), anyOrNull()) + assertThat(event.firstValue.name).isEqualTo("task.duration") + } + + @Test + fun `long task timer is denied when both outputs are ignored without matching its base name`() { + options.metrics.setIgnoredMetrics(listOf("task.active", "task.duration")) + LongTaskTimer.builder("task").register(registry).start() + assertThat(registry.meters).isEmpty() + } + + @Test + fun `rules added after registration still apply through the core filter`() { + val counter = registry.counter("logback.events") + options.metrics.addIgnoredMetric("logback.events") + counter.increment() + verify(client, never()).captureMetric(any(), any(), anyOrNull()) + assertThat(counter.count()).isEqualTo(1.0) + options.metrics.setIgnoredMetrics(emptyList()) + counter.increment() + verify(client).captureMetric(any(), any(), anyOrNull()) + } + + @Test + fun `ignored metrics affect only the Sentry registry in a composite`() { + options.metrics.addIgnoredMetric("logback.events") + val other = SimpleMeterRegistry() + val composite = CompositeMeterRegistry() + try { + composite.add(registry) + composite.add(other) + composite.counter("logback.events").increment(2.0) + assertThat(other.counter("logback.events").count()).isEqualTo(2.0) + assertThat(registry.meters).isEmpty() + verify(client, never()).captureMetric(any(), any(), anyOrNull()) + } finally { + composite.close() + other.close() + } + } + + @Test + fun `time meter filters use the base unit applied after registration filtering`() { + registry + .config() + .namingConvention( + object : NamingConvention { + override fun name(name: String, type: Meter.Type, baseUnit: String?) = "${name}_$baseUnit" + } + ) + options.metrics.addIgnoredMetric("blocked_milliseconds.*") + registry.timer("blocked").record(1, TimeUnit.SECONDS) + LongTaskTimer.builder("blocked").register(registry).start() + assertThat(registry.meters).isEmpty() + + options.metrics.setIgnoredMetrics(listOf("allowed_null.*")) + registry.timer("allowed").record(1, TimeUnit.SECONDS) + val event = argumentCaptor() + verify(client).captureMetric(event.capture(), any(), anyOrNull()) + assertThat(event.firstValue.name).isEqualTo("allowed_milliseconds") + } + + @Test + fun `gauge filter preserves both possible unit-dependent names`() { + registry + .config() + .namingConvention( + object : NamingConvention { + override fun name(name: String, type: Meter.Type, baseUnit: String?) = "${name}_$baseUnit" + } + ) + options.metrics.setIgnoredMetrics(listOf("time_null", "plain_milliseconds")) + val state = AtomicInteger(1) + TimeGauge.builder("time", state, TimeUnit.SECONDS) { it.get().toDouble() }.register(registry) + Gauge.builder("plain", state) { it.get().toDouble() }.register(registry) + registry.pollMeters() + val events = argumentCaptor() + verify(client, times(2)).captureMetric(events.capture(), any(), anyOrNull()) + assertThat(events.allValues.map { it.name }).containsExactly("time_milliseconds", "plain_null") + } + + @Test + fun `nonmatching filter remains neutral so user filters can still deny meters`() { + options.metrics.addIgnoredMetric("other") + registry.config().meterFilter(MeterFilter.denyNameStartsWith("logback")) + registry.counter("logback.events").increment() + assertThat(registry.meters).isEmpty() + } +} diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryAutoConfiguration.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryAutoConfiguration.java index 813477a8e4..1a679e8f14 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryAutoConfiguration.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryAutoConfiguration.java @@ -175,6 +175,11 @@ static class MicrometerConfiguration {} final @NotNull SentryProperties options, final @NotNull ObjectProvider spanFactory, final @NotNull ObjectProvider gitProperties) { + if (options.getMetrics().getIgnoredMetrics() == null) { + options + .getMetrics() + .setIgnoredMetrics(Arrays.asList("logback[.]events", "log4j2[.]events")); + } optionsConfigurations.forEach( optionsConfiguration -> optionsConfiguration.configure(options)); gitProperties.ifAvailable( diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt index ef1f12aeec..b1a2a1778f 100644 --- a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt @@ -244,6 +244,7 @@ class SentryAutoConfigurationTest { "sentry.cron.default-failure-issue-threshold=40", "sentry.cron.default-recovery-threshold=50", "sentry.logs.enabled=true", + "sentry.metrics.ignored-metrics=logback.events,jvm[.].*", "sentry.strict-trace-continuation=true", "sentry.org-id=12345", ) @@ -301,6 +302,8 @@ class SentryAutoConfigurationTest { assertThat(options.cron!!.defaultFailureIssueThreshold).isEqualTo(40L) assertThat(options.cron!!.defaultRecoveryThreshold).isEqualTo(50L) assertThat(options.logs.isEnabled).isEqualTo(true) + assertThat(options.metrics.ignoredMetrics) + .containsExactly(FilterString("logback.events"), FilterString("jvm[.].*")) assertThat(options.isStrictTraceContinuation).isEqualTo(true) assertThat(options.orgId).isEqualTo("12345") } diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryMicrometerConfigurationTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryMicrometerConfigurationTest.kt index e0f2632cba..b9971136f7 100644 --- a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryMicrometerConfigurationTest.kt +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryMicrometerConfigurationTest.kt @@ -5,6 +5,7 @@ import io.micrometer.core.instrument.composite.CompositeMeterRegistry import io.micrometer.core.instrument.simple.SimpleMeterRegistry import io.sentry.IScopes import io.sentry.Sentry +import io.sentry.SentryOptions import io.sentry.micrometer.SentryMeterRegistry import kotlin.test.AfterTest import kotlin.test.Test @@ -46,6 +47,121 @@ class SentryMicrometerConfigurationTest { } } + @Test + fun `logging metrics are ignored by default before meter registration`() { + contextRunner + .withPropertyValues( + "sentry.micrometer.enabled=true", + "sentry.micrometer.poll-interval-millis=0", + ) + .run { + val registry = it.getBean(SentryMeterRegistry::class.java) + assertThat( + it.getBean(IScopes::class.java).options.metrics.ignoredMetrics?.map { it.filterString } + ) + .containsExactly("logback[.]events", "log4j2[.]events") + for (name in listOf("logback.events", "log4j2.events")) { + val counter = registry.counter(name) + counter.increment() + assertThat(counter.count()).isZero() + assertThat(registry.find(name).counter()).isNull() + } + for (name in listOf("business.operations", "logbackXevents", "log4j2Xevents")) { + val counter = registry.counter(name) + counter.increment() + assertThat(counter.count()).isEqualTo(1.0) + } + } + } + + @Test + fun `empty ignored metrics property disables logging defaults`() { + contextRunner + .withPropertyValues("sentry.micrometer.enabled=true", "sentry.metrics.ignored-metrics=") + .run { + assertThat(it.getBean(IScopes::class.java).options.metrics.ignoredMetrics).isEmpty() + val registry = it.getBean(SentryMeterRegistry::class.java) + for (name in listOf("logback.events", "log4j2.events")) { + val counter = registry.counter(name) + counter.increment() + assertThat(counter.count()).isEqualTo(1.0) + } + } + } + + @Test + fun `custom ignored metrics property replaces logging defaults`() { + contextRunner + .withPropertyValues( + "sentry.micrometer.enabled=true", + "sentry.metrics.ignored-metrics=business[.]operations", + ) + .run { + assertThat( + it.getBean(IScopes::class.java).options.metrics.ignoredMetrics?.map { it.filterString } + ) + .containsExactly("business[.]operations") + val registry = it.getBean(SentryMeterRegistry::class.java) + val business = registry.counter("business.operations") + business.increment() + assertThat(business.count()).isZero() + for (name in listOf("logback.events", "log4j2.events")) { + val counter = registry.counter(name) + counter.increment() + assertThat(counter.count()).isEqualTo(1.0) + } + } + } + + @Test + fun `options callback can append to logging defaults`() { + contextRunner + .withBean( + Sentry.OptionsConfiguration::class.java, + { + Sentry.OptionsConfiguration { options -> + options.metrics.addIgnoredMetric("business[.]operations") + } + }, + ) + .run { + assertThat( + it.getBean(IScopes::class.java).options.metrics.ignoredMetrics?.map { it.filterString } + ) + .containsExactly("logback[.]events", "log4j2[.]events", "business[.]operations") + } + } + + @Test + fun `options callback can replace or clear logging defaults`() { + for (names in listOf(null, emptyList(), listOf("business[.]operations"))) { + contextRunner + .withPropertyValues("sentry.micrometer.enabled=true") + .withBean( + Sentry.OptionsConfiguration::class.java, + { + Sentry.OptionsConfiguration { options -> + options.metrics.setIgnoredMetrics(names) + } + }, + ) + .run { + assertThat( + it.getBean(IScopes::class.java).options.metrics.ignoredMetrics?.map { + it.filterString + } + ) + .isEqualTo(names) + val registry = it.getBean(SentryMeterRegistry::class.java) + for (name in listOf("logback.events", "log4j2.events")) { + val counter = registry.counter(name) + counter.increment() + assertThat(counter.count()).isEqualTo(1.0) + } + } + } + } + @Test fun `integration binds properties and depends on initialized Sentry scopes`() { var registry: SentryMeterRegistry? = null @@ -132,6 +248,11 @@ class SentryMicrometerConfigurationTest { assertThat(sentry.get("requests").counter().count()).isEqualTo(1.0) assertThat(simple.get("requests").counter().count()).isEqualTo(1.0) + for (name in listOf("logback.events", "log4j2.events")) { + primary.counter(name).increment() + assertThat(sentry.find(name).counter()).isNull() + assertThat(simple.get(name).counter().count()).isEqualTo(1.0) + } } } diff --git a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java index de037bbfbe..1a9ce7ad72 100644 --- a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java +++ b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java @@ -177,6 +177,11 @@ static class MicrometerConfiguration {} final @NotNull SentryProperties options, final @NotNull ObjectProvider spanFactory, final @NotNull ObjectProvider gitProperties) { + if (options.getMetrics().getIgnoredMetrics() == null) { + options + .getMetrics() + .setIgnoredMetrics(Arrays.asList("logback[.]events", "log4j2[.]events")); + } optionsConfigurations.forEach( optionsConfiguration -> optionsConfiguration.configure(options)); gitProperties.ifAvailable( diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt index 91677d16b4..f708e88977 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt @@ -246,6 +246,7 @@ class SentryAutoConfigurationTest { "sentry.cron.default-failure-issue-threshold=40", "sentry.cron.default-recovery-threshold=50", "sentry.logs.enabled=true", + "sentry.metrics.ignored-metrics=logback.events,jvm[.].*", "sentry.profile-session-sample-rate=1.0", "sentry.profiling-traces-dir-path=tmp/sentry/profiling-traces", "sentry.profile-lifecycle=TRACE", @@ -305,6 +306,8 @@ class SentryAutoConfigurationTest { assertThat(options.cron!!.defaultFailureIssueThreshold).isEqualTo(40L) assertThat(options.cron!!.defaultRecoveryThreshold).isEqualTo(50L) assertThat(options.logs.isEnabled).isEqualTo(true) + assertThat(options.metrics.ignoredMetrics) + .containsExactly(FilterString("logback.events"), FilterString("jvm[.].*")) assertThat(options.profileSessionSampleRate).isEqualTo(1.0) assertThat(options.profilingTracesDirPath) .startsWith(File("tmp/sentry/profiling-traces").absolutePath) diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryMicrometerConfigurationTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryMicrometerConfigurationTest.kt index 6ca4f0eead..51e97cb3f8 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryMicrometerConfigurationTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryMicrometerConfigurationTest.kt @@ -5,6 +5,7 @@ import io.micrometer.core.instrument.composite.CompositeMeterRegistry import io.micrometer.core.instrument.simple.SimpleMeterRegistry import io.sentry.IScopes import io.sentry.Sentry +import io.sentry.SentryOptions import io.sentry.micrometer.SentryMeterRegistry import kotlin.test.AfterTest import kotlin.test.Test @@ -46,6 +47,121 @@ class SentryMicrometerConfigurationTest { } } + @Test + fun `logging metrics are ignored by default before meter registration`() { + contextRunner + .withPropertyValues( + "sentry.micrometer.enabled=true", + "sentry.micrometer.poll-interval-millis=0", + ) + .run { + val registry = it.getBean(SentryMeterRegistry::class.java) + assertThat( + it.getBean(IScopes::class.java).options.metrics.ignoredMetrics?.map { it.filterString } + ) + .containsExactly("logback[.]events", "log4j2[.]events") + for (name in listOf("logback.events", "log4j2.events")) { + val counter = registry.counter(name) + counter.increment() + assertThat(counter.count()).isZero() + assertThat(registry.find(name).counter()).isNull() + } + for (name in listOf("business.operations", "logbackXevents", "log4j2Xevents")) { + val counter = registry.counter(name) + counter.increment() + assertThat(counter.count()).isEqualTo(1.0) + } + } + } + + @Test + fun `empty ignored metrics property disables logging defaults`() { + contextRunner + .withPropertyValues("sentry.micrometer.enabled=true", "sentry.metrics.ignored-metrics=") + .run { + assertThat(it.getBean(IScopes::class.java).options.metrics.ignoredMetrics).isEmpty() + val registry = it.getBean(SentryMeterRegistry::class.java) + for (name in listOf("logback.events", "log4j2.events")) { + val counter = registry.counter(name) + counter.increment() + assertThat(counter.count()).isEqualTo(1.0) + } + } + } + + @Test + fun `custom ignored metrics property replaces logging defaults`() { + contextRunner + .withPropertyValues( + "sentry.micrometer.enabled=true", + "sentry.metrics.ignored-metrics=business[.]operations", + ) + .run { + assertThat( + it.getBean(IScopes::class.java).options.metrics.ignoredMetrics?.map { it.filterString } + ) + .containsExactly("business[.]operations") + val registry = it.getBean(SentryMeterRegistry::class.java) + val business = registry.counter("business.operations") + business.increment() + assertThat(business.count()).isZero() + for (name in listOf("logback.events", "log4j2.events")) { + val counter = registry.counter(name) + counter.increment() + assertThat(counter.count()).isEqualTo(1.0) + } + } + } + + @Test + fun `options callback can append to logging defaults`() { + contextRunner + .withBean( + Sentry.OptionsConfiguration::class.java, + { + Sentry.OptionsConfiguration { options -> + options.metrics.addIgnoredMetric("business[.]operations") + } + }, + ) + .run { + assertThat( + it.getBean(IScopes::class.java).options.metrics.ignoredMetrics?.map { it.filterString } + ) + .containsExactly("logback[.]events", "log4j2[.]events", "business[.]operations") + } + } + + @Test + fun `options callback can replace or clear logging defaults`() { + for (names in listOf(null, emptyList(), listOf("business[.]operations"))) { + contextRunner + .withPropertyValues("sentry.micrometer.enabled=true") + .withBean( + Sentry.OptionsConfiguration::class.java, + { + Sentry.OptionsConfiguration { options -> + options.metrics.setIgnoredMetrics(names) + } + }, + ) + .run { + assertThat( + it.getBean(IScopes::class.java).options.metrics.ignoredMetrics?.map { + it.filterString + } + ) + .isEqualTo(names) + val registry = it.getBean(SentryMeterRegistry::class.java) + for (name in listOf("logback.events", "log4j2.events")) { + val counter = registry.counter(name) + counter.increment() + assertThat(counter.count()).isEqualTo(1.0) + } + } + } + } + @Test fun `integration binds properties and depends on initialized Sentry scopes`() { var registry: SentryMeterRegistry? = null @@ -132,6 +248,11 @@ class SentryMicrometerConfigurationTest { assertThat(sentry.get("requests").counter().count()).isEqualTo(1.0) assertThat(simple.get("requests").counter().count()).isEqualTo(1.0) + for (name in listOf("logback.events", "log4j2.events")) { + primary.counter(name).increment() + assertThat(sentry.find(name).counter()).isNull() + assertThat(simple.get(name).counter().count()).isEqualTo(1.0) + } } } diff --git a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java index 42a254b351..6f51d6164d 100644 --- a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java +++ b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java @@ -172,6 +172,11 @@ static class MicrometerConfiguration {} final @NotNull SentryProperties options, final @NotNull ObjectProvider spanFactory, final @NotNull ObjectProvider gitProperties) { + if (options.getMetrics().getIgnoredMetrics() == null) { + options + .getMetrics() + .setIgnoredMetrics(Arrays.asList("logback[.]events", "log4j2[.]events")); + } optionsConfigurations.forEach( optionsConfiguration -> optionsConfiguration.configure(options)); gitProperties.ifAvailable( diff --git a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt index d9e598d047..c7f56c7357 100644 --- a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt @@ -244,6 +244,7 @@ class SentryAutoConfigurationTest { "sentry.cron.default-failure-issue-threshold=40", "sentry.cron.default-recovery-threshold=50", "sentry.logs.enabled=true", + "sentry.metrics.ignored-metrics=logback.events,jvm[.].*", "sentry.profile-session-sample-rate=1.0", "sentry.profiling-traces-dir-path=tmp/sentry/profiling-traces", "sentry.profile-lifecycle=TRACE", @@ -303,6 +304,8 @@ class SentryAutoConfigurationTest { assertThat(options.cron!!.defaultFailureIssueThreshold).isEqualTo(40L) assertThat(options.cron!!.defaultRecoveryThreshold).isEqualTo(50L) assertThat(options.logs.isEnabled).isEqualTo(true) + assertThat(options.metrics.ignoredMetrics) + .containsExactly(FilterString("logback.events"), FilterString("jvm[.].*")) assertThat(options.profileSessionSampleRate).isEqualTo(1.0) assertThat(options.profilingTracesDirPath) .startsWith(File("tmp/sentry/profiling-traces").absolutePath) diff --git a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryMicrometerConfigurationTest.kt b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryMicrometerConfigurationTest.kt index 503e0cf804..81e813acf4 100644 --- a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryMicrometerConfigurationTest.kt +++ b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryMicrometerConfigurationTest.kt @@ -5,6 +5,7 @@ import io.micrometer.core.instrument.composite.CompositeMeterRegistry import io.micrometer.core.instrument.simple.SimpleMeterRegistry import io.sentry.IScopes import io.sentry.Sentry +import io.sentry.SentryOptions import io.sentry.micrometer.SentryMeterRegistry import kotlin.test.AfterTest import kotlin.test.Test @@ -46,6 +47,121 @@ class SentryMicrometerConfigurationTest { } } + @Test + fun `logging metrics are ignored by default before meter registration`() { + contextRunner + .withPropertyValues( + "sentry.micrometer.enabled=true", + "sentry.micrometer.poll-interval-millis=0", + ) + .run { + val registry = it.getBean(SentryMeterRegistry::class.java) + assertThat( + it.getBean(IScopes::class.java).options.metrics.ignoredMetrics?.map { it.filterString } + ) + .containsExactly("logback[.]events", "log4j2[.]events") + for (name in listOf("logback.events", "log4j2.events")) { + val counter = registry.counter(name) + counter.increment() + assertThat(counter.count()).isZero() + assertThat(registry.find(name).counter()).isNull() + } + for (name in listOf("business.operations", "logbackXevents", "log4j2Xevents")) { + val counter = registry.counter(name) + counter.increment() + assertThat(counter.count()).isEqualTo(1.0) + } + } + } + + @Test + fun `empty ignored metrics property disables logging defaults`() { + contextRunner + .withPropertyValues("sentry.micrometer.enabled=true", "sentry.metrics.ignored-metrics=") + .run { + assertThat(it.getBean(IScopes::class.java).options.metrics.ignoredMetrics).isEmpty() + val registry = it.getBean(SentryMeterRegistry::class.java) + for (name in listOf("logback.events", "log4j2.events")) { + val counter = registry.counter(name) + counter.increment() + assertThat(counter.count()).isEqualTo(1.0) + } + } + } + + @Test + fun `custom ignored metrics property replaces logging defaults`() { + contextRunner + .withPropertyValues( + "sentry.micrometer.enabled=true", + "sentry.metrics.ignored-metrics=business[.]operations", + ) + .run { + assertThat( + it.getBean(IScopes::class.java).options.metrics.ignoredMetrics?.map { it.filterString } + ) + .containsExactly("business[.]operations") + val registry = it.getBean(SentryMeterRegistry::class.java) + val business = registry.counter("business.operations") + business.increment() + assertThat(business.count()).isZero() + for (name in listOf("logback.events", "log4j2.events")) { + val counter = registry.counter(name) + counter.increment() + assertThat(counter.count()).isEqualTo(1.0) + } + } + } + + @Test + fun `options callback can append to logging defaults`() { + contextRunner + .withBean( + Sentry.OptionsConfiguration::class.java, + { + Sentry.OptionsConfiguration { options -> + options.metrics.addIgnoredMetric("business[.]operations") + } + }, + ) + .run { + assertThat( + it.getBean(IScopes::class.java).options.metrics.ignoredMetrics?.map { it.filterString } + ) + .containsExactly("logback[.]events", "log4j2[.]events", "business[.]operations") + } + } + + @Test + fun `options callback can replace or clear logging defaults`() { + for (names in listOf(null, emptyList(), listOf("business[.]operations"))) { + contextRunner + .withPropertyValues("sentry.micrometer.enabled=true") + .withBean( + Sentry.OptionsConfiguration::class.java, + { + Sentry.OptionsConfiguration { options -> + options.metrics.setIgnoredMetrics(names) + } + }, + ) + .run { + assertThat( + it.getBean(IScopes::class.java).options.metrics.ignoredMetrics?.map { + it.filterString + } + ) + .isEqualTo(names) + val registry = it.getBean(SentryMeterRegistry::class.java) + for (name in listOf("logback.events", "log4j2.events")) { + val counter = registry.counter(name) + counter.increment() + assertThat(counter.count()).isEqualTo(1.0) + } + } + } + } + @Test fun `integration binds properties and depends on initialized Sentry scopes`() { var registry: SentryMeterRegistry? = null @@ -132,6 +248,11 @@ class SentryMicrometerConfigurationTest { assertThat(sentry.get("requests").counter().count()).isEqualTo(1.0) assertThat(simple.get("requests").counter().count()).isEqualTo(1.0) + for (name in listOf("logback.events", "log4j2.events")) { + primary.counter(name).increment() + assertThat(sentry.find(name).counter()).isNull() + assertThat(simple.get(name).counter().count()).isEqualTo(1.0) + } } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index e6b83beb21..2912e5b90d 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -504,6 +504,7 @@ public final class io/sentry/ExternalOptions { public fun getIgnoredCheckIns ()Ljava/util/List; public fun getIgnoredErrors ()Ljava/util/List; public fun getIgnoredExceptionsForType ()Ljava/util/Set; + public fun getIgnoredMetrics ()Ljava/util/List; public fun getIgnoredTransactions ()Ljava/util/List; public fun getInAppExcludes ()Ljava/util/List; public fun getInAppIncludes ()Ljava/util/List; @@ -563,6 +564,7 @@ public final class io/sentry/ExternalOptions { public fun setIdleTimeout (Ljava/lang/Long;)V public fun setIgnoredCheckIns (Ljava/util/List;)V public fun setIgnoredErrors (Ljava/util/List;)V + public fun setIgnoredMetrics (Ljava/util/List;)V public fun setIgnoredTransactions (Ljava/util/List;)V public fun setMaxRequestBodySize (Lio/sentry/SentryOptions$RequestSize;)V public fun setOrgId (Ljava/lang/String;)V @@ -4019,11 +4021,14 @@ public abstract interface class io/sentry/SentryOptions$Logs$BeforeSendLogCallba public final class io/sentry/SentryOptions$Metrics { public fun ()V + public fun addIgnoredMetric (Ljava/lang/String;)V public fun getBeforeSend ()Lio/sentry/SentryOptions$Metrics$BeforeSendMetricCallback; + public fun getIgnoredMetrics ()Ljava/util/List; public fun getMetricsBatchProcessorFactory ()Lio/sentry/metrics/IMetricsBatchProcessorFactory; public fun isEnabled ()Z public fun setBeforeSend (Lio/sentry/SentryOptions$Metrics$BeforeSendMetricCallback;)V public fun setEnabled (Z)V + public fun setIgnoredMetrics (Ljava/util/List;)V public fun setMetricsBatchProcessorFactory (Lio/sentry/metrics/IMetricsBatchProcessorFactory;)V } @@ -7999,6 +8004,10 @@ public final class io/sentry/util/MapObjectWriter : io/sentry/ObjectWriter { public fun value (Z)Lio/sentry/util/MapObjectWriter; } +public final class io/sentry/util/MetricsUtils { + public static fun isIgnored (Ljava/util/List;Ljava/lang/String;)Z +} + public final class io/sentry/util/Objects { public static fun equals (Ljava/lang/Object;Ljava/lang/Object;)Z public static fun hash ([Ljava/lang/Object;)I diff --git a/sentry/src/main/java/io/sentry/ExternalOptions.java b/sentry/src/main/java/io/sentry/ExternalOptions.java index 4e44ea422e..9659d55e7f 100644 --- a/sentry/src/main/java/io/sentry/ExternalOptions.java +++ b/sentry/src/main/java/io/sentry/ExternalOptions.java @@ -48,6 +48,7 @@ public final class ExternalOptions { private @Nullable Boolean enableSpotlight; private @Nullable Boolean enableLogs; private @Nullable Boolean enableMetrics; + private @Nullable List ignoredMetrics; private @Nullable String spotlightConnectionUrl; private @Nullable List ignoredCheckIns; @@ -179,6 +180,7 @@ public final class ExternalOptions { options.setEnableLogs(propertiesProvider.getBooleanProperty("logs.enabled")); options.setEnableMetrics(propertiesProvider.getBooleanProperty("metrics.enabled")); + options.setIgnoredMetrics(propertiesProvider.getListOrNull("metrics.ignored-metrics")); for (final String ignoredExceptionType : propertiesProvider.getList("ignored-exceptions-for-type")) { @@ -623,6 +625,14 @@ public void setEnableMetrics(final @Nullable Boolean enableMetrics) { return enableMetrics; } + public @Nullable List getIgnoredMetrics() { + return ignoredMetrics; + } + + public void setIgnoredMetrics(final @Nullable List ignoredMetrics) { + this.ignoredMetrics = ignoredMetrics; + } + public @Nullable Double getProfileSessionSampleRate() { return profileSessionSampleRate; } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 1a1fbf738c..c72cbf2f6e 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -3786,6 +3786,9 @@ public void merge(final @NotNull ExternalOptions options) { if (options.isEnableMetrics() != null) { getMetrics().setEnabled(options.isEnableMetrics()); } + if (options.getIgnoredMetrics() != null) { + getMetrics().setIgnoredMetrics(options.getIgnoredMetrics()); + } if (options.getProfileSessionSampleRate() != null) { setProfileSessionSampleRate(options.getProfileSessionSampleRate()); @@ -4091,6 +4094,53 @@ public static final class Metrics { private @NotNull IMetricsBatchProcessorFactory metricsBatchProcessorFactory = new DefaultMetricsBatchProcessorFactory(); + private @Nullable List ignoredMetrics; + + /** + * Returns the filters applied to final metric names. No metrics are ignored by default. + * + * @return the configured name filters, or null if unset + */ + public @Nullable List getIgnoredMetrics() { + return ignoredMetrics; + } + + /** + * Sets metric names or regular expressions to ignore before processing metrics. Exact matches + * are case-insensitive; regular expressions must match the entire name. This applies to both + * manual and automatically captured metrics. + * + *

Micrometer also uses these filters when registering meters. Changing the list does not + * remove existing meters or reactivate meters previously denied registration. + * + * @param ignoredMetrics the names or regex patterns, or null to clear the filters + */ + public void setIgnoredMetrics(final @Nullable List ignoredMetrics) { + if (ignoredMetrics == null) { + this.ignoredMetrics = null; + } else { + final List filters = new ArrayList<>(); + for (final String name : ignoredMetrics) { + if (name != null && !name.isEmpty()) { + filters.add(new FilterString(name)); + } + } + this.ignoredMetrics = filters; + } + } + + /** + * Adds a metric name or regular expression to ignore. + * + * @param ignoredMetric the name or regex pattern + */ + public void addIgnoredMetric(final @NotNull String ignoredMetric) { + if (ignoredMetrics == null) { + ignoredMetrics = new ArrayList<>(); + } + ignoredMetrics.add(new FilterString(ignoredMetric)); + } + /** * Whether Sentry Metrics feature is enabled and metrics are sent to Sentry. * diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java index cebcad9735..fdaad9ab51 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java @@ -17,6 +17,7 @@ import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryId; import io.sentry.protocol.User; +import io.sentry.util.MetricsUtils; import io.sentry.util.Platform; import io.sentry.util.TracingUtils; import java.util.HashMap; @@ -127,7 +128,7 @@ private void captureMetrics( return; } - if (name == null) { + if (name == null || MetricsUtils.isIgnored(options.getMetrics().getIgnoredMetrics(), name)) { return; } diff --git a/sentry/src/main/java/io/sentry/util/MetricsUtils.java b/sentry/src/main/java/io/sentry/util/MetricsUtils.java new file mode 100644 index 0000000000..c517188af7 --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/MetricsUtils.java @@ -0,0 +1,30 @@ +package io.sentry.util; + +import io.sentry.FilterString; +import java.util.List; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class MetricsUtils { + private MetricsUtils() {} + + /** Checks a final metric name against case-insensitive exact matches and full regex matches. */ + public static boolean isIgnored( + final @Nullable List ignoredMetrics, final @Nullable String name) { + if (name == null || ignoredMetrics == null || ignoredMetrics.isEmpty()) { + return false; + } + for (final FilterString ignoredMetric : ignoredMetrics) { + if (ignoredMetric.getFilterString().equalsIgnoreCase(name)) { + return true; + } + } + for (final FilterString ignoredMetric : ignoredMetrics) { + if (ignoredMetric.matches(name)) { + return true; + } + } + return false; + } +} diff --git a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt index fee707d31f..5e15a2668f 100644 --- a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt @@ -1,5 +1,6 @@ package io.sentry +import com.google.common.truth.Truth.assertThat import io.sentry.config.PropertiesProviderFactory import java.lang.RuntimeException import kotlin.test.Test @@ -15,6 +16,28 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.verify class ExternalOptionsTest { + @Test + fun `reads ignored metrics from external properties`() { + withPropertiesFile("metrics.ignored-metrics=logback.events,jvm[.].*") { + assertThat(it.ignoredMetrics).containsExactly("logback.events", "jvm[.].*") + } + } + + @Test + fun `ignored metrics remain unset when external property is absent`() { + withPropertiesFile { assertThat(it.ignoredMetrics).isNull() } + } + + @Test + fun `empty external ignored metrics clear configured filters`() { + withPropertiesFile("metrics.ignored-metrics=") { external -> + val options = SentryOptions() + options.metrics.addIgnoredMetric("logback.events") + options.merge(external) + assertThat(options.metrics.ignoredMetrics).isEmpty() + } + } + @Test fun `creates options with proxy using external properties`() { withPropertiesFile( diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index d1cb38c649..8fbc4ff095 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -3306,6 +3306,53 @@ class ScopesTest { // region metrics + @Test + fun `ignored metric names are dropped for all metric types before timestamps and capture`() { + val (sut, client) = getEnabledScopes { it.metrics.setIgnoredMetrics(listOf("ignored[.].*")) } + val dateProvider = mock() + sut.options.setDateProvider(dateProvider) + + sut.metrics().count("ignored.counter") + sut.metrics().distribution("ignored.distribution", 1.0) + sut.metrics().gauge("ignored.gauge", 1.0) + + verify(dateProvider, never()).now() + verify(client, never()).captureMetric(any(), anyOrNull(), anyOrNull()) + } + + @Test + fun `ignored metric names apply to both manual and integration origins`() { + val (sut, client) = getEnabledScopes { it.metrics.addIgnoredMetric("logback.events") } + sut.metrics().count("logback.events") + sut + .metrics() + .count( + "LOGBACK.EVENTS", + 1.0, + null, + SentryMetricsParameters.create(null, null).apply { origin = "auto.metrics.micrometer" }, + ) + verify(client, never()).captureMetric(any(), anyOrNull(), anyOrNull()) + } + + @Test + fun `ignored metric rules do not drop other names`() { + val (sut, client) = getEnabledScopes { it.metrics.addIgnoredMetric("logback.events") } + sut.metrics().count("business.operations") + verify(client).captureMetric(any(), anyOrNull(), anyOrNull()) + } + + @Test + fun `changing ignored metric rules affects subsequent captures`() { + val (sut, client) = getEnabledScopes() + sut.metrics().count("metric") + sut.options.metrics.addIgnoredMetric("metric") + sut.metrics().count("metric") + sut.options.metrics.setIgnoredMetrics(null) + sut.metrics().count("metric") + verify(client, times(2)).captureMetric(any(), anyOrNull(), anyOrNull()) + } + @Test fun `when captureMetric is called on disabled client, do nothing`() { val (sut, mockClient) = getEnabledScopes() diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 64482b5d5d..1a63dcecc2 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -1,5 +1,6 @@ package io.sentry +import com.google.common.truth.Truth.assertThat import io.sentry.SentryOptions.RequestSize import io.sentry.logger.ILoggerBatchProcessorFactory import io.sentry.util.StringUtils @@ -20,6 +21,48 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.verify class SentryOptionsTest { + @Test + fun `no metrics are ignored by default`() { + assertThat(SentryOptions().metrics.ignoredMetrics).isNull() + } + + @Test + fun `ignored metrics can be added replaced and cleared`() { + val metrics = SentryOptions().metrics + metrics.addIgnoredMetric("logback.events") + assertThat(metrics.ignoredMetrics).containsExactly(FilterString("logback.events")) + val names = mutableListOf("jvm[.].*", "", "metric[") + metrics.setIgnoredMetrics(names) + names.clear() + assertThat(metrics.ignoredMetrics) + .containsExactly(FilterString("jvm[.].*"), FilterString("metric[")) + metrics.setIgnoredMetrics(emptyList()) + assertThat(metrics.ignoredMetrics).isEmpty() + metrics.setIgnoredMetrics(null) + assertThat(metrics.ignoredMetrics).isNull() + } + + @Test + fun `external ignored metrics replace existing filters and are copied`() { + val options = SentryOptions() + options.metrics.addIgnoredMetric("old") + val names = mutableListOf("logback.events", "jvm[.].*") + options.merge(ExternalOptions().apply { ignoredMetrics = names }) + names.clear() + assertThat(options.metrics.ignoredMetrics) + .containsExactly(FilterString("logback.events"), FilterString("jvm[.].*")) + } + + @Test + fun `unset external ignored metrics preserve filters but empty list clears them`() { + val options = SentryOptions() + options.metrics.addIgnoredMetric("logback.events") + options.merge(ExternalOptions()) + assertThat(options.metrics.ignoredMetrics).containsExactly(FilterString("logback.events")) + options.merge(ExternalOptions().apply { ignoredMetrics = emptyList() }) + assertThat(options.metrics.ignoredMetrics).isEmpty() + } + @Test fun `when options is initialized, logger is not null`() { assertNotNull(SentryOptions().logger) diff --git a/sentry/src/test/java/io/sentry/util/MetricsUtilsTest.kt b/sentry/src/test/java/io/sentry/util/MetricsUtilsTest.kt new file mode 100644 index 0000000000..83f0fad08f --- /dev/null +++ b/sentry/src/test/java/io/sentry/util/MetricsUtilsTest.kt @@ -0,0 +1,48 @@ +package io.sentry.util + +import com.google.common.truth.Truth.assertThat +import io.sentry.FilterString +import kotlin.test.Test + +class MetricsUtilsTest { + @Test + fun `no filters or no name does not ignore metrics`() { + assertThat(MetricsUtils.isIgnored(null, "logback.events")).isFalse() + assertThat(MetricsUtils.isIgnored(emptyList(), "logback.events")).isFalse() + assertThat(MetricsUtils.isIgnored(listOf(FilterString(".*")), null)).isFalse() + } + + @Test + fun `exact matches are case insensitive`() { + assertThat(MetricsUtils.isIgnored(listOf(FilterString("LOGBACK.EVENTS")), "logback.events")) + .isTrue() + } + + @Test + fun `regex matches the full name and respects regex case sensitivity`() { + val filters = listOf(FilterString("logback[.].*")) + assertThat(MetricsUtils.isIgnored(filters, "logback.events")).isTrue() + assertThat(MetricsUtils.isIgnored(filters, "prefix.logback.events")).isFalse() + assertThat(MetricsUtils.isIgnored(filters, "LOGBACK.events")).isFalse() + assertThat(MetricsUtils.isIgnored(listOf(FilterString("(?i)logback[.].*")), "LOGBACK.events")) + .isTrue() + } + + @Test + fun `invalid regex still supports exact matches without throwing`() { + val filters = listOf(FilterString("metric[")) + assertThat(MetricsUtils.isIgnored(filters, "METRIC[")).isTrue() + assertThat(MetricsUtils.isIgnored(filters, "other")).isFalse() + } + + @Test + fun `decisions are not shared between option sets or stale after changes`() { + val filters = mutableListOf(FilterString("ignored")) + assertThat(MetricsUtils.isIgnored(filters, "ignored")).isTrue() + assertThat(MetricsUtils.isIgnored(listOf(FilterString("other")), "ignored")).isFalse() + filters.clear() + filters.add(FilterString("allowed")) + assertThat(MetricsUtils.isIgnored(filters, "ignored")).isFalse() + assertThat(MetricsUtils.isIgnored(filters, "allowed")).isTrue() + } +} From 20e3db60226e45a789daeb27317ba50ee5c343ea Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 23 Sep 2026 14:43:25 +0200 Subject: [PATCH 2/3] changelog Link the ignored-metrics feature entry and Spring Boot defaults to PR #6155. Co-Authored-By: Codex --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a946415fdb..9ae5e2ab58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Features - Add a Micrometer metrics integration with opt-in Spring Boot support ([#6116](https://github.com/getsentry/sentry-java/pull/6116)) -- Add `options.getMetrics().setIgnoredMetrics(...)` to filter metric names before processing, including early Micrometer filtering +- Add `options.getMetrics().setIgnoredMetrics(...)` to filter metric names before processing, including early Micrometer filtering ([#6155](https://github.com/getsentry/sentry-java/pull/6155)) - Configure names or full regular-expression patterns through the Java API, Android manifest, `sentry.properties`, or Spring Boot's `application.properties`. Use `[.]` to match a literal dot in a pattern. - **Spring Boot 2, 3, and 4 default:** ignore `logback.events` and `log4j2.events` metrics to avoid logging-driven metric queue overload. An explicit list replaces these defaults; an empty list disables filtering. Outside Spring Boot, no metric names are ignored by default. Actual log messages are unaffected. - **AndroidManifest.xml:** add `io.sentry.metrics.ignored-metrics` under ``: From 99384d97f1bd044b6aace29c943657588226e5f3 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 23 Sep 2026 16:02:41 +0200 Subject: [PATCH 3/3] fix(micrometer): Scope logging defaults to enabled integration Apply the default logging metric exclusions only when the Micrometer integration is enabled. This keeps manual metrics unchanged for Spring Boot applications that have not opted into Micrometer export. Cover disabled and enabled behavior across all supported Boot versions and align the integration documentation with the scoped defaults. Co-Authored-By: Claude --- sentry-micrometer/README.md | 4 ++-- .../java/io/sentry/spring/boot4/SentryAutoConfiguration.java | 2 +- .../sentry/spring/boot4/SentryMicrometerConfigurationTest.kt | 3 +++ .../sentry/spring/boot/jakarta/SentryAutoConfiguration.java | 2 +- .../spring/boot/jakarta/SentryMicrometerConfigurationTest.kt | 3 +++ .../java/io/sentry/spring/boot/SentryAutoConfiguration.java | 2 +- .../sentry/spring/boot/SentryMicrometerConfigurationTest.kt | 3 +++ 7 files changed, 14 insertions(+), 5 deletions(-) diff --git a/sentry-micrometer/README.md b/sentry-micrometer/README.md index b8d1eb122a..d1ed737588 100644 --- a/sentry-micrometer/README.md +++ b/sentry-micrometer/README.md @@ -142,8 +142,8 @@ sentry.metrics.ignored-metrics= Defaults are applied before `Sentry.OptionsConfiguration` callbacks. A callback can append filters with `options.getMetrics().addIgnoredMetric(...)`, replace them with `setIgnoredMetrics(...)`, or clear them with an empty list or `null`. Enabled external configuration is merged afterward. -These Boot defaults apply even when Micrometer export is disabled, so manually recorded Sentry -metrics with the same names are also filtered. +These Boot defaults apply only when Micrometer export is enabled. While enabled, manually recorded +Sentry metrics with the same names are also filtered. For `sentry.properties`, use `metrics.ignored-metrics`; the environment variable is `SENTRY_METRICS_IGNORED_METRICS`. Android supports the manifest metadata key diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryAutoConfiguration.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryAutoConfiguration.java index 1a679e8f14..0d0f5eb781 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryAutoConfiguration.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryAutoConfiguration.java @@ -175,7 +175,7 @@ static class MicrometerConfiguration {} final @NotNull SentryProperties options, final @NotNull ObjectProvider spanFactory, final @NotNull ObjectProvider gitProperties) { - if (options.getMetrics().getIgnoredMetrics() == null) { + if (options.getMicrometer().isEnabled() && options.getMetrics().getIgnoredMetrics() == null) { options .getMetrics() .setIgnoredMetrics(Arrays.asList("logback[.]events", "log4j2[.]events")); diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryMicrometerConfigurationTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryMicrometerConfigurationTest.kt index 6b672da08d..d4c07ec093 100644 --- a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryMicrometerConfigurationTest.kt +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryMicrometerConfigurationTest.kt @@ -48,9 +48,11 @@ class SentryMicrometerConfigurationTest { assertThat(it.getBean(SentryProperties::class.java).micrometer.isEnabled).isFalse() assertThat(it.getBean(SentryProperties::class.java).micrometer.pollIntervalMillis) .isEqualTo(60_000) + assertThat(it.getBean(SentryProperties::class.java).metrics.ignoredMetrics).isNull() } contextRunner.withPropertyValues("sentry.micrometer.enabled=false").run { assertThat(it).doesNotHaveBean(SentryMeterRegistry::class.java) + assertThat(it.getBean(SentryProperties::class.java).metrics.ignoredMetrics).isNull() } } @@ -123,6 +125,7 @@ class SentryMicrometerConfigurationTest { @Test fun `options callback can append to logging defaults`() { contextRunner + .withPropertyValues("sentry.micrometer.enabled=true") .withBean( Sentry.OptionsConfiguration::class.java, { diff --git a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java index 1a9ce7ad72..70b85a306f 100644 --- a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java +++ b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java @@ -177,7 +177,7 @@ static class MicrometerConfiguration {} final @NotNull SentryProperties options, final @NotNull ObjectProvider spanFactory, final @NotNull ObjectProvider gitProperties) { - if (options.getMetrics().getIgnoredMetrics() == null) { + if (options.getMicrometer().isEnabled() && options.getMetrics().getIgnoredMetrics() == null) { options .getMetrics() .setIgnoredMetrics(Arrays.asList("logback[.]events", "log4j2[.]events")); diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryMicrometerConfigurationTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryMicrometerConfigurationTest.kt index ad3cc20e92..d5ae1629b5 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryMicrometerConfigurationTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryMicrometerConfigurationTest.kt @@ -48,9 +48,11 @@ class SentryMicrometerConfigurationTest { assertThat(it.getBean(SentryProperties::class.java).micrometer.isEnabled).isFalse() assertThat(it.getBean(SentryProperties::class.java).micrometer.pollIntervalMillis) .isEqualTo(60_000) + assertThat(it.getBean(SentryProperties::class.java).metrics.ignoredMetrics).isNull() } contextRunner.withPropertyValues("sentry.micrometer.enabled=false").run { assertThat(it).doesNotHaveBean(SentryMeterRegistry::class.java) + assertThat(it.getBean(SentryProperties::class.java).metrics.ignoredMetrics).isNull() } } @@ -123,6 +125,7 @@ class SentryMicrometerConfigurationTest { @Test fun `options callback can append to logging defaults`() { contextRunner + .withPropertyValues("sentry.micrometer.enabled=true") .withBean( Sentry.OptionsConfiguration::class.java, { diff --git a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java index 6f51d6164d..bf90c3b51e 100644 --- a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java +++ b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java @@ -172,7 +172,7 @@ static class MicrometerConfiguration {} final @NotNull SentryProperties options, final @NotNull ObjectProvider spanFactory, final @NotNull ObjectProvider gitProperties) { - if (options.getMetrics().getIgnoredMetrics() == null) { + if (options.getMicrometer().isEnabled() && options.getMetrics().getIgnoredMetrics() == null) { options .getMetrics() .setIgnoredMetrics(Arrays.asList("logback[.]events", "log4j2[.]events")); diff --git a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryMicrometerConfigurationTest.kt b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryMicrometerConfigurationTest.kt index 057914aa33..aa056c03e9 100644 --- a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryMicrometerConfigurationTest.kt +++ b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryMicrometerConfigurationTest.kt @@ -48,9 +48,11 @@ class SentryMicrometerConfigurationTest { assertThat(it.getBean(SentryProperties::class.java).micrometer.isEnabled).isFalse() assertThat(it.getBean(SentryProperties::class.java).micrometer.pollIntervalMillis) .isEqualTo(60_000) + assertThat(it.getBean(SentryProperties::class.java).metrics.ignoredMetrics).isNull() } contextRunner.withPropertyValues("sentry.micrometer.enabled=false").run { assertThat(it).doesNotHaveBean(SentryMeterRegistry::class.java) + assertThat(it.getBean(SentryProperties::class.java).metrics.ignoredMetrics).isNull() } } @@ -123,6 +125,7 @@ class SentryMicrometerConfigurationTest { @Test fun `options callback can append to logging defaults`() { contextRunner + .withPropertyValues("sentry.micrometer.enabled=true") .withBean( Sentry.OptionsConfiguration::class.java, {