Skip to content

Commit

Permalink
Add metrics API (#3205)
Browse files Browse the repository at this point in the history
* Implement different metric types, add API and aggregator

* Fix typos, make executorService volatile

* Move from Calendar to long timestamps, add hooks for testing

* Rename to metrics, have Sentry.metrics() as entry point

* Add more tests, use cr32 for hashing strings

* Enrich tags, add more tests

* Cleanup tests and timing API, remove uneeded threadlocal

* Update changelog

* Move default tag generation to IMetricsHub, improve dx

* Cleanup

* Cleanup

* Fix remove duplicate metricAggregator.close call

* Address PR feedback

* Move test into metrics helper

* Rename getValues to serialize to match API spec

* Add support for force-flushing metrics when weight is too high

* Update Changelog

* Revert "Add support for force-flushing metrics when weight is too high"

This reverts commit 6a3be45.

* Fix .api

* Fix tests

* Fix remove test code

* Replace tag values with empty string

* Address PR feedback

* Format & API

* Fix tests

* Force flush metrics when aggregator exceeds max weight (#3220)

* Update Changelog

* Revert "Revert "Add support for force-flushing metrics when weight is too high""

This reverts commit 07fc4e5.

* Fix changelog

* Address PR feedback

* Fix tests

* Fix remove test code

* Address PR feedback

* Update Changelog
  • Loading branch information
markushi authored Feb 28, 2024
1 parent 9578eab commit 39e3ed7
Show file tree
Hide file tree
Showing 42 changed files with 3,251 additions and 15 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- Fix hub restore point in wrappers: SentryWrapper, SentryTaskDecorator and SentryScheduleHook ([#3225](https://github.com/getsentry/sentry-java/pull/3225))
- We now reset the hub to its previous value on the thread where the `Runnable`/`Callable`/`Supplier` is executed instead of setting it to the hub that was used on the thread where the `Runnable`/`Callable`/`Supplier` was created.
- Fix add missing thread name/id to app start spans ([#3226](https://github.com/getsentry/sentry-java/pull/3226))
- Experimental: Add Metrics API ([#3205](https://github.com/getsentry/sentry-java/pull/3205))

## 7.4.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1024,7 +1024,7 @@ class ActivityLifecycleIntegrationTest {
// Assert the ttfd span is running and a timeout autoCancel task has been scheduled
assertNotNull(ttfdSpan)
assertFalse(ttfdSpan.isFinished)
assertTrue(deferredExecutorService.scheduledRunnables.isNotEmpty())
assertTrue(deferredExecutorService.hasScheduledRunnables())

// Run the autoClose task and assert the ttfd span is finished with deadlineExceeded
deferredExecutorService.runAll()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.sentry.CheckIn
import io.sentry.Hint
import io.sentry.IMetricsAggregator
import io.sentry.IScope
import io.sentry.ISentryClient
import io.sentry.ProfilingTraceData
Expand Down Expand Up @@ -174,5 +175,9 @@ class SessionTrackingIntegrationTest {
override fun getRateLimiter(): RateLimiter? {
TODO("Not yet implemented")
}

override fun getMetricsAggregator(): IMetricsAggregator {
TODO("Not yet implemented")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import android.app.Application;
import android.os.StrictMode;
import io.sentry.Sentry;

/** Apps. main Application. */
public class MyApplication extends Application {
Expand All @@ -24,6 +25,8 @@ public void onCreate() {
// });
// */
// });

Sentry.metrics().increment("app.start.cold");
}

private void strictMode() {
Expand Down
2 changes: 1 addition & 1 deletion sentry-test-support/api/sentry-test-support.api
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public final class io/sentry/SkipError : java/lang/Error {
public final class io/sentry/test/DeferredExecutorService : io/sentry/ISentryExecutorService {
public fun <init> ()V
public fun close (J)V
public final fun getScheduledRunnables ()Ljava/util/ArrayList;
public final fun hasScheduledRunnables ()Z
public fun isClosed ()Z
public final fun runAll ()V
public fun schedule (Ljava/lang/Runnable;J)Ljava/util/concurrent/Future;
Expand Down
28 changes: 22 additions & 6 deletions sentry-test-support/src/main/kotlin/io/sentry/test/Mocks.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,40 @@ class ImmediateExecutorService : ISentryExecutorService {

class DeferredExecutorService : ISentryExecutorService {

private val runnables = ArrayList<Runnable>()
val scheduledRunnables = ArrayList<Runnable>()
private var runnables = ArrayList<Runnable>()
private var scheduledRunnables = ArrayList<Runnable>()

fun runAll() {
runnables.forEach { it.run() }
scheduledRunnables.forEach { it.run() }
// take a snapshot of the runnable list in case
// executing the runnable itself schedules more runnables
val currentRunnableList = runnables
val currentScheduledRunnableList = scheduledRunnables

synchronized(this) {
runnables = ArrayList()
scheduledRunnables = ArrayList()
}

currentRunnableList.forEach { it.run() }
currentScheduledRunnableList.forEach { it.run() }
}

override fun submit(runnable: Runnable): Future<*> {
runnables.add(runnable)
synchronized(this) {
runnables.add(runnable)
}
return mock()
}

override fun <T> submit(callable: Callable<T>): Future<T> = mock()
override fun schedule(runnable: Runnable, delayMillis: Long): Future<*> {
scheduledRunnables.add(runnable)
synchronized(this) {
scheduledRunnables.add(runnable)
}
return mock()
}
override fun close(timeoutMillis: Long) {}
override fun isClosed(): Boolean = false

fun hasScheduledRunnables(): Boolean = scheduledRunnables.isNotEmpty()
}
205 changes: 203 additions & 2 deletions sentry/api/sentry.api

Large diffs are not rendered by default.

27 changes: 25 additions & 2 deletions sentry/src/main/java/io/sentry/Hub.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.sentry.clientreport.DiscardReason;
import io.sentry.hints.SessionEndHint;
import io.sentry.hints.SessionStartHint;
import io.sentry.metrics.MetricsApi;
import io.sentry.protocol.SentryId;
import io.sentry.protocol.SentryTransaction;
import io.sentry.protocol.User;
Expand All @@ -17,14 +18,16 @@
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public final class Hub implements IHub {
public final class Hub implements IHub, MetricsApi.IMetricsInterface {

private volatile @NotNull SentryId lastEventId;
private final @NotNull SentryOptions options;
private volatile boolean isEnabled;
Expand All @@ -33,10 +36,10 @@ public final class Hub implements IHub {
private final @NotNull Map<Throwable, Pair<WeakReference<ISpan>, String>> throwableToSpan =
Collections.synchronizedMap(new WeakHashMap<>());
private final @NotNull TransactionPerformanceCollector transactionPerformanceCollector;
private final @NotNull MetricsApi metricsApi;

public Hub(final @NotNull SentryOptions options) {
this(options, createRootStackItem(options));

// Integrations are no longer registered on Hub ctor, but on Sentry.init
}

Expand All @@ -52,6 +55,8 @@ private Hub(final @NotNull SentryOptions options, final @NotNull Stack stack) {
// Integrations will use this Hub instance once registered.
// Make sure Hub ready to be used then.
this.isEnabled = true;

this.metricsApi = new MetricsApi(this);
}

private Hub(final @NotNull SentryOptions options, final @NotNull StackItem rootStackItem) {
Expand Down Expand Up @@ -937,4 +942,22 @@ private IScope buildLocalScope(
final StackItem item = stack.peek();
return item.getClient().getRateLimiter();
}

@Override
public @NotNull MetricsApi metrics() {
return metricsApi;
}

@Override
public @NotNull IMetricsAggregator getMetricsAggregator() {
return stack.peek().getClient().getMetricsAggregator();
}

@Override
public @NotNull Map<String, String> getDefaultTagsForMetrics() {
final Map<String, String> tags = new HashMap<>(2);
tags.put("release", options.getRelease());
tags.put("environment", options.getEnvironment());
return tags;
}
}
6 changes: 6 additions & 0 deletions sentry/src/main/java/io/sentry/HubAdapter.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.sentry;

import io.sentry.metrics.MetricsApi;
import io.sentry.protocol.SentryId;
import io.sentry.protocol.SentryTransaction;
import io.sentry.protocol.User;
Expand Down Expand Up @@ -272,4 +273,9 @@ public void reportFullyDisplayed() {
public @Nullable RateLimiter getRateLimiter() {
return Sentry.getCurrentHub().getRateLimiter();
}

@Override
public @NotNull MetricsApi metrics() {
return Sentry.getCurrentHub().metrics();
}
}
5 changes: 5 additions & 0 deletions sentry/src/main/java/io/sentry/IHub.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.sentry;

import io.sentry.metrics.MetricsApi;
import io.sentry.protocol.SentryId;
import io.sentry.protocol.SentryTransaction;
import io.sentry.protocol.User;
Expand Down Expand Up @@ -582,4 +583,8 @@ TransactionContext continueTrace(
@ApiStatus.Internal
@Nullable
RateLimiter getRateLimiter();

@ApiStatus.Experimental
@NotNull
MetricsApi metrics();
}
122 changes: 122 additions & 0 deletions sentry/src/main/java/io/sentry/IMetricsAggregator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package io.sentry;

import java.io.Closeable;
import java.util.Map;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public interface IMetricsAggregator extends Closeable {

/**
* Emits a Counter metric
*
* @param key A unique key identifying the metric
* @param value The value to be added
* @param unit An optional unit, see {@link MeasurementUnit}
* @param tags Optional Tags to associate with the metric
* @param timestampMs The time when the metric was emitted. Defaults to the time at which the
* metric is emitted, if no value is provided.
* @param stackLevel Optional number of stacks levels to ignore when determining the code location
*/
void increment(
final @NotNull String key,
final double value,
final @Nullable MeasurementUnit unit,
final @Nullable Map<String, String> tags,
final long timestampMs,
final int stackLevel);

/**
* Emits a Gauge metric
*
* @param key A unique key identifying the metric
* @param value The value to be added
* @param unit An optional unit, see {@link MeasurementUnit}
* @param tags Optional Tags to associate with the metric
* @param timestampMs The time when the metric was emitted. Defaults to the time at which the
* metric is emitted, if no value is provided.
* @param stackLevel Optional number of stacks levels to ignore when determining the code location
*/
void gauge(
final @NotNull String key,
final double value,
final @Nullable MeasurementUnit unit,
final @Nullable Map<String, String> tags,
final long timestampMs,
final int stackLevel);

/**
* Emits a Distribution metric
*
* @param key A unique key identifying the metric
* @param value The value to be added
* @param unit An optional unit, see {@link MeasurementUnit}
* @param tags Optional Tags to associate with the metric
* @param timestampMs The time when the metric was emitted. Defaults to the time at which the
* metric is emitted, if no value is provided.
* @param stackLevel Optional number of stacks levels to ignore when determining the code location
*/
void distribution(
final @NotNull String key,
final double value,
final @Nullable MeasurementUnit unit,
final @Nullable Map<String, String> tags,
final long timestampMs,
final int stackLevel);

/**
* Emits a Set metric
*
* @param key A unique key identifying the metric
* @param value The value to be added
* @param unit An optional unit, see {@link MeasurementUnit}
* @param tags Optional Tags to associate with the metric
* @param timestampMs The time when the metric was emitted. Defaults to the time at which the
* metric is emitted, if no value is provided.
* @param stackLevel Optional number of stacks levels to ignore when determining the code location
*/
void set(
final @NotNull String key,
final int value,
final @Nullable MeasurementUnit unit,
final @Nullable Map<String, String> tags,
final long timestampMs,
final int stackLevel);

/**
* Emits a Set metric
*
* @param key A unique key identifying the metric
* @param value The value to be added
* @param unit An optional unit, see {@link MeasurementUnit}
* @param tags Optional Tags to associate with the metric
* @param timestampMs The time when the metric was emitted. Defaults to the time at which the
* metric is emitted, if no value is provided.
* @param stackLevel Optional number of stacks levels to ignore when determining the code location
*/
void set(
final @NotNull String key,
final @NotNull String value,
final @Nullable MeasurementUnit unit,
final @Nullable Map<String, String> tags,
final long timestampMs,
final int stackLevel);

/**
* Emits a distribution with the time it takes to run a given code block.
*
* @param key A unique key identifying the metric
* @param callback The code block to measure
* @param unit An optional unit, see {@link MeasurementUnit.Duration}, defaults to seconds
* @param tags Optional Tags to associate with the metric
* @param stackLevel Optional number of stacks levels to ignore when determining the code location
*/
void timing(
final @NotNull String key,
final @NotNull Runnable callback,
final @NotNull MeasurementUnit.Duration unit,
final @Nullable Map<String, String> tags,
final int stackLevel);

void flush(boolean force);
}
4 changes: 4 additions & 0 deletions sentry/src/main/java/io/sentry/ISentryClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -285,4 +285,8 @@ SentryId captureTransaction(
default boolean isHealthy() {
return true;
}

@ApiStatus.Internal
@NotNull
IMetricsAggregator getMetricsAggregator();
}
Loading

0 comments on commit 39e3ed7

Please sign in to comment.