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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ([#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 `<application>`:
```xml
<meta-data
android:name="io.sentry.metrics.ignored-metrics"
android:value="noisy[.]metric" />
```
- **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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -717,6 +718,10 @@ static void applyMetadata(
.getMetrics()
.setEnabled(
readBool(metadata, logger, ENABLE_METRICS, options.getMetrics().isEnabled()));
final @Nullable List<String> ignoredMetrics = readList(metadata, logger, IGNORED_METRICS);
if (ignoredMetrics != null) {
options.getMetrics().setIgnoredMetrics(ignoredMetrics);
}

final @NotNull SentryFeedbackOptions feedbackOptions = options.getFeedbackOptions();
feedbackOptions.setNameRequired(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 58 additions & 6 deletions sentry-micrometer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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鈥攊ncluding HTTP server, JVM,
process, and logging metrics鈥攁re 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鈥攁re 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

Expand Down Expand Up @@ -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 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
`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"));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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.IScopes;
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;
private final @NotNull IScopes scopes;

SentryIgnoredMetricsFilter(
final @NotNull SentryMeterRegistry registry, final @NotNull IScopes scopes) {
this.registry = registry;
this.scopes = scopes;
}

@Override
public @NotNull MeterFilterReply accept(final @NotNull Meter.Id id) {
final @Nullable List<FilterString> ignoredMetrics =
scopes.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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,10 @@ public SentryMeterRegistry(final @NotNull IScopes scopes, final long pollInterva
"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, scopes))
.onMeterRemoved(this::onMeterRemoved);
addIntegrationToSdkVersion(INTEGRATION_NAME);
if (this.scheduler == null) {
pollingTask = null;
Expand Down
Loading
Loading