|
13 | 13 | import io.micrometer.core.instrument.Meter; |
14 | 14 | import io.micrometer.core.instrument.MeterRegistry; |
15 | 15 | import io.micrometer.core.instrument.Tag; |
| 16 | +import io.micrometer.core.instrument.TimeGauge; |
16 | 17 | import io.micrometer.core.instrument.Timer; |
17 | | -import io.micrometer.core.instrument.cumulative.CumulativeFunctionCounter; |
18 | 18 | import io.micrometer.core.instrument.cumulative.CumulativeFunctionTimer; |
19 | 19 | import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; |
20 | 20 | import io.micrometer.core.instrument.distribution.pause.PauseDetector; |
21 | 21 | import io.micrometer.core.instrument.internal.DefaultGauge; |
22 | 22 | import io.micrometer.core.instrument.internal.DefaultLongTaskTimer; |
23 | 23 | import io.micrometer.core.instrument.internal.DefaultMeter; |
| 24 | +import io.micrometer.core.instrument.util.NamedThreadFactory; |
24 | 25 | import io.sentry.Sentry; |
25 | 26 | import io.sentry.SentryAttributes; |
26 | 27 | import io.sentry.SentryIntegrationPackageStorage; |
27 | 28 | import io.sentry.SentryLevel; |
28 | 29 | import io.sentry.metrics.MetricsUnit; |
| 30 | +import io.sentry.util.ExceptionUtils; |
29 | 31 | import java.util.HashMap; |
30 | 32 | import java.util.Map; |
| 33 | +import java.util.concurrent.Executors; |
| 34 | +import java.util.concurrent.ScheduledExecutorService; |
| 35 | +import java.util.concurrent.ScheduledFuture; |
31 | 36 | import java.util.concurrent.TimeUnit; |
32 | 37 | import java.util.function.ToDoubleFunction; |
33 | 38 | import java.util.function.ToLongFunction; |
|
37 | 42 | /** A Micrometer registry that forwards metrics to Sentry. */ |
38 | 43 | public final class SentryMeterRegistry extends MeterRegistry { |
39 | 44 | private static final @NotNull String INTEGRATION_NAME = "Micrometer"; |
| 45 | + private static final long DEFAULT_POLL_INTERVAL_MILLIS = 60_000; |
| 46 | + |
| 47 | + private final @Nullable ScheduledExecutorService scheduler; |
| 48 | + private final @Nullable ScheduledFuture<?> pollingTask; |
40 | 49 |
|
41 | 50 | static { |
42 | 51 | SentryIntegrationPackageStorage.getInstance() |
43 | 52 | .addPackage("maven:io.sentry:sentry-micrometer", BuildConfig.VERSION_NAME); |
44 | 53 | } |
45 | 54 |
|
46 | | - /** Creates a registry that forwards active meter observations to Sentry. */ |
| 55 | + /** Creates a registry that polls passive meters every 60 seconds. */ |
47 | 56 | public SentryMeterRegistry() { |
48 | | - super(Clock.SYSTEM); |
| 57 | + this(DEFAULT_POLL_INTERVAL_MILLIS); |
| 58 | + } |
| 59 | + |
| 60 | + /** |
| 61 | + * Creates a registry with the given passive meter polling interval in milliseconds. |
| 62 | + * |
| 63 | + * <p>A zero interval disables passive meter polling. Negative intervals are not supported. |
| 64 | + */ |
| 65 | + public SentryMeterRegistry(final long pollIntervalMillis) { |
| 66 | + this(pollIntervalMillis, Clock.SYSTEM, createScheduler(pollIntervalMillis)); |
| 67 | + } |
| 68 | + |
| 69 | + SentryMeterRegistry( |
| 70 | + final long pollIntervalMillis, |
| 71 | + final @NotNull Clock clock, |
| 72 | + final @Nullable ScheduledExecutorService scheduler) { |
| 73 | + super(clock); |
| 74 | + validatePollInterval(pollIntervalMillis); |
| 75 | + if (pollIntervalMillis > 0 && scheduler == null) { |
| 76 | + throw new IllegalArgumentException( |
| 77 | + "A scheduler is required when passive polling is enabled."); |
| 78 | + } |
| 79 | + this.scheduler = pollIntervalMillis == 0 ? null : scheduler; |
| 80 | + config().onMeterRemoved(this::onMeterRemoved); |
49 | 81 | addIntegrationToSdkVersion(INTEGRATION_NAME); |
| 82 | + if (this.scheduler == null) { |
| 83 | + pollingTask = null; |
| 84 | + } else { |
| 85 | + pollingTask = |
| 86 | + this.scheduler.scheduleAtFixedRate( |
| 87 | + this::pollMeters, pollIntervalMillis, pollIntervalMillis, TimeUnit.MILLISECONDS); |
| 88 | + } |
50 | 89 | } |
51 | 90 |
|
52 | 91 | @Override |
@@ -110,7 +149,7 @@ public SentryMeterRegistry() { |
110 | 149 | final @NotNull Meter.Id id, |
111 | 150 | final @NotNull T obj, |
112 | 151 | final @NotNull ToDoubleFunction<T> countFunction) { |
113 | | - return new CumulativeFunctionCounter<>(id, obj, countFunction); |
| 152 | + return new SentryFunctionCounter<>(id, obj, countFunction, this, createMetricInfo(id)); |
114 | 153 | } |
115 | 154 |
|
116 | 155 | @Override |
@@ -157,16 +196,127 @@ void captureDistribution(final @NotNull SentryMetricInfo metricInfo, final doubl |
157 | 196 | metricInfo.getName(), value, metricInfo.getUnit(), metricInfo.createParameters()); |
158 | 197 | } |
159 | 198 |
|
| 199 | + void captureGauge(final @NotNull SentryMetricInfo metricInfo, final double value) { |
| 200 | + if (isClosed()) { |
| 201 | + return; |
| 202 | + } |
| 203 | + Sentry.getCurrentScopes() |
| 204 | + .metrics() |
| 205 | + .gauge(metricInfo.getName(), value, metricInfo.getUnit(), metricInfo.createParameters()); |
| 206 | + } |
| 207 | + |
| 208 | + void pollMeters() { |
| 209 | + if (isClosed()) { |
| 210 | + return; |
| 211 | + } |
| 212 | + for (final @NotNull Meter meter : getMeters()) { |
| 213 | + if (isClosed()) { |
| 214 | + return; |
| 215 | + } |
| 216 | + try { |
| 217 | + publishPassiveMeter(meter); |
| 218 | + } catch (Throwable throwable) { |
| 219 | + ExceptionUtils.rethrowIfFatal(throwable); |
| 220 | + Sentry.getCurrentScopes() |
| 221 | + .getOptions() |
| 222 | + .getLogger() |
| 223 | + .log( |
| 224 | + SentryLevel.DEBUG, |
| 225 | + throwable, |
| 226 | + "Failed to publish Micrometer meter %s to Sentry.", |
| 227 | + meter.getId().getName()); |
| 228 | + } |
| 229 | + } |
| 230 | + } |
| 231 | + |
| 232 | + private void publishPassiveMeter(final @NotNull Meter meter) { |
| 233 | + if (meter instanceof TimeGauge) { |
| 234 | + publishTimeGauge((TimeGauge) meter); |
| 235 | + } else if (meter instanceof Gauge) { |
| 236 | + publishGauge((Gauge) meter); |
| 237 | + } else if (meter instanceof LongTaskTimer) { |
| 238 | + publishLongTaskTimer((LongTaskTimer) meter); |
| 239 | + } else if (meter instanceof SentryFunctionCounter) { |
| 240 | + ((SentryFunctionCounter<?>) meter).poll(); |
| 241 | + } |
| 242 | + } |
| 243 | + |
| 244 | + private void publishGauge(final @NotNull Gauge gauge) { |
| 245 | + final double value = gauge.value(); |
| 246 | + if (Double.isFinite(value)) { |
| 247 | + captureGauge(createMetricInfo(gauge.getId()), value); |
| 248 | + } |
| 249 | + } |
| 250 | + |
| 251 | + private void publishTimeGauge(final @NotNull TimeGauge gauge) { |
| 252 | + final double value = gauge.value(TimeUnit.MILLISECONDS); |
| 253 | + if (Double.isFinite(value)) { |
| 254 | + captureGauge(createMetricInfo(gauge.getId(), MetricsUnit.Duration.MILLISECOND), value); |
| 255 | + } |
| 256 | + } |
| 257 | + |
| 258 | + private void publishLongTaskTimer(final @NotNull LongTaskTimer timer) { |
| 259 | + final @NotNull SentryMetricInfo activeMetric = createMetricInfo(timer.getId(), ".active", null); |
| 260 | + captureGauge(activeMetric, timer.activeTasks()); |
| 261 | + |
| 262 | + final double duration = timer.duration(TimeUnit.MILLISECONDS); |
| 263 | + if (Double.isFinite(duration)) { |
| 264 | + captureGauge( |
| 265 | + createMetricInfo(timer.getId(), ".duration", MetricsUnit.Duration.MILLISECOND), duration); |
| 266 | + } |
| 267 | + } |
| 268 | + |
| 269 | + private void onMeterRemoved(final @NotNull Meter meter) { |
| 270 | + if (meter instanceof SentryFunctionCounter) { |
| 271 | + ((SentryFunctionCounter<?>) meter).markRemoved(); |
| 272 | + } |
| 273 | + } |
| 274 | + |
160 | 275 | private @NotNull SentryMetricInfo createMetricInfo(final @NotNull Meter.Id id) { |
161 | 276 | return createMetricInfo(id, SentryMetricUnit.normalize(id.getBaseUnit())); |
162 | 277 | } |
163 | 278 |
|
164 | 279 | private @NotNull SentryMetricInfo createMetricInfo( |
165 | 280 | final @NotNull Meter.Id id, final @Nullable String unit) { |
| 281 | + return createMetricInfo(id, "", unit); |
| 282 | + } |
| 283 | + |
| 284 | + private @NotNull SentryMetricInfo createMetricInfo( |
| 285 | + final @NotNull Meter.Id id, final @NotNull String suffix, final @Nullable String unit) { |
166 | 286 | final @NotNull Map<String, Object> attributes = new HashMap<>(); |
167 | 287 | for (final @NotNull Tag tag : getConventionTags(id)) { |
168 | 288 | attributes.put(tag.getKey(), tag.getValue()); |
169 | 289 | } |
170 | | - return new SentryMetricInfo(getConventionName(id), unit, SentryAttributes.fromMap(attributes)); |
| 290 | + return new SentryMetricInfo( |
| 291 | + getConventionName(id) + suffix, unit, SentryAttributes.fromMap(attributes)); |
| 292 | + } |
| 293 | + |
| 294 | + @Override |
| 295 | + public void close() { |
| 296 | + if (isClosed()) { |
| 297 | + return; |
| 298 | + } |
| 299 | + super.close(); |
| 300 | + if (pollingTask != null) { |
| 301 | + pollingTask.cancel(true); |
| 302 | + } |
| 303 | + if (scheduler != null) { |
| 304 | + scheduler.shutdownNow(); |
| 305 | + } |
| 306 | + } |
| 307 | + |
| 308 | + private static @Nullable ScheduledExecutorService createScheduler(final long pollIntervalMillis) { |
| 309 | + validatePollInterval(pollIntervalMillis); |
| 310 | + if (pollIntervalMillis == 0) { |
| 311 | + return null; |
| 312 | + } |
| 313 | + return Executors.newSingleThreadScheduledExecutor( |
| 314 | + new NamedThreadFactory("sentry-micrometer-poller")); |
| 315 | + } |
| 316 | + |
| 317 | + private static void validatePollInterval(final long pollIntervalMillis) { |
| 318 | + if (pollIntervalMillis < 0) { |
| 319 | + throw new IllegalArgumentException("The passive meter polling interval cannot be negative."); |
| 320 | + } |
171 | 321 | } |
172 | 322 | } |
0 commit comments