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
12 changes: 9 additions & 3 deletions sentry-micrometer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,16 @@ Passive meters are polled every 60 seconds by default:
| `LongTaskTimer` active tasks | `${name}.active` gauge |
| `LongTaskTimer` active duration | `${name}.duration` gauge in milliseconds |
| `FunctionCounter` | Positive counter delta |
| `FunctionTimer` count | `${name}.count` positive counter delta |
| `FunctionTimer` total time | `${name}.total_time` positive counter delta in milliseconds |

The first successful finite `FunctionCounter` poll establishes its baseline and emits nothing.
Later positive deltas are sent. A decreasing value is treated as a reset and establishes a new
baseline.
The first successful finite function-meter poll establishes its baseline and emits nothing. Later
positive deltas are sent. A decreasing value is treated as a reset and establishes a new baseline.
`FunctionTimer` tracks its count and total-time baselines independently.

A `FunctionTimer` exposes only cumulative count and total time, not individual duration
observations. The integration therefore exports these values as counter deltas rather than a mean
gauge or distribution. Percentiles cannot be reconstructed from count and total time alone.

Unsupported custom meters remain readable through Micrometer but are not exported to Sentry.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package io.sentry.micrometer;

import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.cumulative.CumulativeFunctionTimer;
import io.sentry.util.ExceptionUtils;
import java.util.concurrent.TimeUnit;
import java.util.function.ToDoubleFunction;
import java.util.function.ToLongFunction;
import org.jetbrains.annotations.NotNull;

final class SentryFunctionTimer<T> extends CumulativeFunctionTimer<T>
implements SentryRemovableMeter {
private final @NotNull SentryMeterRegistry registry;
private final @NotNull SentryMetricInfo countMetricInfo;
private final @NotNull SentryMetricInfo totalTimeMetricInfo;
private volatile boolean removed;
private boolean countInitialized;
private double previousCount;
private boolean totalTimeInitialized;
private double previousTotalTime;

SentryFunctionTimer(
final @NotNull Meter.Id id,
final @NotNull T obj,
final @NotNull ToLongFunction<T> countFunction,
final @NotNull ToDoubleFunction<T> totalTimeFunction,
final @NotNull TimeUnit totalTimeFunctionUnit,
final @NotNull TimeUnit baseTimeUnit,
final @NotNull SentryMeterRegistry registry,
final @NotNull SentryMetricInfo countMetricInfo,
final @NotNull SentryMetricInfo totalTimeMetricInfo) {
super(id, obj, countFunction, totalTimeFunction, totalTimeFunctionUnit, baseTimeUnit);
this.registry = registry;
this.countMetricInfo = countMetricInfo;
this.totalTimeMetricInfo = totalTimeMetricInfo;
}

void poll() {
try {
pollCount();
} catch (Throwable throwable) {
ExceptionUtils.rethrowIfFatal(throwable);
registry.logPollingFailure(throwable, countMetricInfo.getName());
}

try {
pollTotalTime();
} catch (Throwable throwable) {
ExceptionUtils.rethrowIfFatal(throwable);
registry.logPollingFailure(throwable, totalTimeMetricInfo.getName());
}
}

private void pollCount() {
final double currentCount = count();
if (!Double.isFinite(currentCount) || removed || registry.isClosed()) {
return;
}

if (!countInitialized || currentCount < previousCount) {
countInitialized = true;
previousCount = currentCount;
return;
}

final double delta = currentCount - previousCount;
previousCount = currentCount;
if (delta > 0.0 && !removed) {
registry.captureCounter(countMetricInfo, delta);
}
}

private void pollTotalTime() {
final double currentTotalTime = totalTime(TimeUnit.MILLISECONDS);
if (!Double.isFinite(currentTotalTime) || removed || registry.isClosed()) {
return;
}

if (!totalTimeInitialized || currentTotalTime < previousTotalTime) {
totalTimeInitialized = true;
previousTotalTime = currentTotalTime;
return;
}

final double delta = currentTotalTime - previousTotalTime;
previousTotalTime = currentTotalTime;
if (delta > 0.0 && !removed) {
registry.captureCounter(totalTimeMetricInfo, delta);
}
}

@Override
public void markRemoved() {
removed = true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import io.micrometer.core.instrument.TimeGauge;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.config.NamingConvention;
import io.micrometer.core.instrument.cumulative.CumulativeFunctionTimer;
import io.micrometer.core.instrument.distribution.DistributionStatisticConfig;
import io.micrometer.core.instrument.distribution.pause.PauseDetector;
import io.micrometer.core.instrument.internal.DefaultGauge;
Expand Down Expand Up @@ -141,8 +140,16 @@ public SentryMeterRegistry(final long pollIntervalMillis) {
final @NotNull ToLongFunction<T> countFunction,
final @NotNull ToDoubleFunction<T> totalTimeFunction,
final @NotNull TimeUnit totalTimeFunctionUnit) {
return new CumulativeFunctionTimer<>(
id, obj, countFunction, totalTimeFunction, totalTimeFunctionUnit, getBaseTimeUnit());
return new SentryFunctionTimer<>(
id,
obj,
countFunction,
totalTimeFunction,
totalTimeFunctionUnit,
getBaseTimeUnit(),
this,
createMetricInfo(id, ".count", null),
createMetricInfo(id, ".total_time", MetricsUnit.Duration.MILLISECOND));
}

@Override
Expand Down Expand Up @@ -218,18 +225,22 @@ void pollMeters() {
publishPassiveMeter(meter);
} catch (Throwable throwable) {
ExceptionUtils.rethrowIfFatal(throwable);
Sentry.getCurrentScopes()
.getOptions()
.getLogger()
.log(
SentryLevel.DEBUG,
throwable,
"Failed to publish Micrometer meter %s to Sentry.",
meter.getId().getName());
logPollingFailure(throwable, meter.getId().getName());
}
}
}

void logPollingFailure(final @NotNull Throwable throwable, final @NotNull String meterName) {
Sentry.getCurrentScopes()
.getOptions()
.getLogger()
.log(
SentryLevel.DEBUG,
throwable,
"Failed to publish Micrometer meter %s to Sentry.",
meterName);
}

private void publishPassiveMeter(final @NotNull Meter meter) {
if (meter instanceof TimeGauge) {
publishTimeGauge((TimeGauge) meter);
Expand All @@ -239,6 +250,8 @@ private void publishPassiveMeter(final @NotNull Meter meter) {
publishLongTaskTimer((LongTaskTimer) meter);
} else if (meter instanceof SentryFunctionCounter) {
((SentryFunctionCounter<?>) meter).poll();
} else if (meter instanceof SentryFunctionTimer) {
((SentryFunctionTimer<?>) meter).poll();
}
}

Expand Down
Loading
Loading