From 4f5b71f00596a6872069d43f0b3f6e600b2180b0 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 14 Feb 2024 10:24:56 +0100 Subject: [PATCH 01/26] Implement different metric types, add API and aggregator --- .../sentry/samples/android/MyApplication.java | 3 + sentry/api/sentry.api | 169 +++++++++++- sentry/src/main/java/io/sentry/Hub.java | 49 +++- .../src/main/java/io/sentry/HubAdapter.java | 6 + sentry/src/main/java/io/sentry/IHub.java | 5 + .../java/io/sentry/IMetricAggregator.java | 127 +++++++++ .../main/java/io/sentry/MetricAggregator.java | 251 ++++++++++++++++++ sentry/src/main/java/io/sentry/NoOpHub.java | 8 + sentry/src/main/java/io/sentry/Sentry.java | 8 + .../java/io/sentry/SentryEnvelopeItem.java | 22 +- .../main/java/io/sentry/SentryItemType.java | 1 + .../java/io/sentry/metrics/CodeLocations.java | 32 +++ .../java/io/sentry/metrics/CounterMetric.java | 49 ++++ .../io/sentry/metrics/DistributionMetric.java | 47 ++++ .../io/sentry/metrics/EncodedMetrics.java | 21 ++ .../java/io/sentry/metrics/GaugeMetric.java | 59 ++++ .../java/io/sentry/metrics/IMetricsHub.java | 20 ++ .../main/java/io/sentry/metrics/Metric.java | 66 +++++ .../java/io/sentry/metrics/MetricHelper.java | 199 ++++++++++++++ .../metrics/MetricResourceIdentifier.java | 64 +++++ .../java/io/sentry/metrics/MetricType.java | 12 + .../java/io/sentry/metrics/MetricsApi.java | 143 ++++++++++ .../sentry/metrics/NoopMetricAggregator.java | 77 ++++++ .../java/io/sentry/metrics/SentryMetric.java | 78 ++++++ .../java/io/sentry/metrics/SetMetric.java | 46 ++++ .../test/java/io/sentry/SentryClientTest.kt | 4 +- .../io/sentry/metrics/CounterMetricTest.kt | 84 ++++++ .../sentry/metrics/DistributionMetricTest.kt | 67 +++++ .../java/io/sentry/metrics/GaugeMetricTest.kt | 73 +++++ .../io/sentry/metrics/MetricHelperTest.kt | 70 +++++ .../java/io/sentry/metrics/SetMetricTest.kt | 69 +++++ 31 files changed, 1923 insertions(+), 6 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/IMetricAggregator.java create mode 100644 sentry/src/main/java/io/sentry/MetricAggregator.java create mode 100644 sentry/src/main/java/io/sentry/metrics/CodeLocations.java create mode 100644 sentry/src/main/java/io/sentry/metrics/CounterMetric.java create mode 100644 sentry/src/main/java/io/sentry/metrics/DistributionMetric.java create mode 100644 sentry/src/main/java/io/sentry/metrics/EncodedMetrics.java create mode 100644 sentry/src/main/java/io/sentry/metrics/GaugeMetric.java create mode 100644 sentry/src/main/java/io/sentry/metrics/IMetricsHub.java create mode 100644 sentry/src/main/java/io/sentry/metrics/Metric.java create mode 100644 sentry/src/main/java/io/sentry/metrics/MetricHelper.java create mode 100644 sentry/src/main/java/io/sentry/metrics/MetricResourceIdentifier.java create mode 100644 sentry/src/main/java/io/sentry/metrics/MetricType.java create mode 100644 sentry/src/main/java/io/sentry/metrics/MetricsApi.java create mode 100644 sentry/src/main/java/io/sentry/metrics/NoopMetricAggregator.java create mode 100644 sentry/src/main/java/io/sentry/metrics/SentryMetric.java create mode 100644 sentry/src/main/java/io/sentry/metrics/SetMetric.java create mode 100644 sentry/src/test/java/io/sentry/metrics/CounterMetricTest.kt create mode 100644 sentry/src/test/java/io/sentry/metrics/DistributionMetricTest.kt create mode 100644 sentry/src/test/java/io/sentry/metrics/GaugeMetricTest.kt create mode 100644 sentry/src/test/java/io/sentry/metrics/MetricHelperTest.kt create mode 100644 sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java index a4a1c5397a..11b47107a9 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java @@ -2,6 +2,7 @@ import android.app.Application; import android.os.StrictMode; +import io.sentry.Sentry; /** Apps. main Application. */ public class MyApplication extends Application { @@ -24,6 +25,8 @@ public void onCreate() { // }); // */ // }); + + Sentry.getMetricsApi().increment("app.start.cold", 1, null, null, null, 0); } private void strictMode() { diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 03cd96ad0d..5a23d7e84b 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -408,7 +408,7 @@ public final class io/sentry/HttpStatusCodeRange { public fun isInRange (I)Z } -public final class io/sentry/Hub : io/sentry/IHub { +public final class io/sentry/Hub : io/sentry/IHub, io/sentry/metrics/IMetricsHub { public fun (Lio/sentry/SentryOptions;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V @@ -421,6 +421,7 @@ public final class io/sentry/Hub : io/sentry/IHub { public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureMetrics (Lio/sentry/metrics/EncodedMetrics;)V public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V public fun clearBreadcrumbs ()V @@ -433,6 +434,7 @@ public final class io/sentry/Hub : io/sentry/IHub { public fun flush (J)V public fun getBaggage ()Lio/sentry/BaggageHeader; public fun getLastEventId ()Lio/sentry/protocol/SentryId; + public fun getMetricsApi ()Lio/sentry/metrics/MetricsApi; public fun getOptions ()Lio/sentry/SentryOptions; public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun getSpan ()Lio/sentry/ISpan; @@ -484,6 +486,7 @@ public final class io/sentry/HubAdapter : io/sentry/IHub { public fun getBaggage ()Lio/sentry/BaggageHeader; public static fun getInstance ()Lio/sentry/HubAdapter; public fun getLastEventId ()Lio/sentry/protocol/SentryId; + public fun getMetricsApi ()Lio/sentry/metrics/MetricsApi; public fun getOptions ()Lio/sentry/SentryOptions; public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun getSpan ()Lio/sentry/ISpan; @@ -573,6 +576,7 @@ public abstract interface class io/sentry/IHub { public abstract fun flush (J)V public abstract fun getBaggage ()Lio/sentry/BaggageHeader; public abstract fun getLastEventId ()Lio/sentry/protocol/SentryId; + public abstract fun getMetricsApi ()Lio/sentry/metrics/MetricsApi; public abstract fun getOptions ()Lio/sentry/SentryOptions; public abstract fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public abstract fun getSpan ()Lio/sentry/ISpan; @@ -614,6 +618,19 @@ public abstract interface class io/sentry/IMemoryCollector { public abstract fun collect ()Lio/sentry/MemoryCollectionData; } +public abstract interface class io/sentry/IMetricAggregator : java/io/Closeable { + public abstract fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V + public abstract fun gauge (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V + public abstract fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V + public abstract fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V + public abstract fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V + public abstract fun timing (Ljava/lang/String;Lio/sentry/IMetricAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;Ljava/util/Calendar;I)V +} + +public abstract interface class io/sentry/IMetricAggregator$TimingCallback { + public abstract fun run ()V +} + public abstract interface class io/sentry/IOptionsObserver { public abstract fun setDist (Ljava/lang/String;)V public abstract fun setEnvironment (Ljava/lang/String;)V @@ -1001,6 +1018,20 @@ public final class io/sentry/MemoryCollectionData { public fun getUsedNativeMemory ()J } +public final class io/sentry/MetricAggregator : io/sentry/IMetricAggregator, java/io/Closeable, java/lang/Runnable { + public fun (Lio/sentry/metrics/IMetricsHub;Lio/sentry/ILogger;)V + public fun close ()V + public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V + public fun flush (Z)V + public fun gauge (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V + public fun getFlushableBuckets (Z)Ljava/util/Set; + public fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V + public fun run ()V + public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V + public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V + public fun timing (Ljava/lang/String;Lio/sentry/IMetricAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;Ljava/util/Calendar;I)V +} + public final class io/sentry/MonitorConfig : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun (Lio/sentry/MonitorSchedule;)V public fun getCheckinMargin ()Ljava/lang/Long; @@ -1138,6 +1169,7 @@ public final class io/sentry/NoOpHub : io/sentry/IHub { public fun getBaggage ()Lio/sentry/BaggageHeader; public static fun getInstance ()Lio/sentry/NoOpHub; public fun getLastEventId ()Lio/sentry/protocol/SentryId; + public fun getMetricsApi ()Lio/sentry/metrics/MetricsApi; public fun getOptions ()Lio/sentry/SentryOptions; public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun getSpan ()Lio/sentry/ISpan; @@ -1686,6 +1718,7 @@ public final class io/sentry/Sentry { public static fun getBaggage ()Lio/sentry/BaggageHeader; public static fun getCurrentHub ()Lio/sentry/IHub; public static fun getLastEventId ()Lio/sentry/protocol/SentryId; + public static fun getMetricsApi ()Lio/sentry/metrics/MetricsApi; public static fun getSpan ()Lio/sentry/ISpan; public static fun getTraceparent ()Lio/sentry/SentryTraceHeader; public static fun init ()V @@ -1922,6 +1955,7 @@ public final class io/sentry/SentryEnvelopeItem { public static fun fromCheckIn (Lio/sentry/ISerializer;Lio/sentry/CheckIn;)Lio/sentry/SentryEnvelopeItem; public static fun fromClientReport (Lio/sentry/ISerializer;Lio/sentry/clientreport/ClientReport;)Lio/sentry/SentryEnvelopeItem; public static fun fromEvent (Lio/sentry/ISerializer;Lio/sentry/SentryBaseEvent;)Lio/sentry/SentryEnvelopeItem; + public static fun fromMetrics (Lio/sentry/metrics/EncodedMetrics;)Lio/sentry/SentryEnvelopeItem; public static fun fromProfilingTrace (Lio/sentry/ProfilingTraceData;JLio/sentry/ISerializer;)Lio/sentry/SentryEnvelopeItem; public static fun fromSession (Lio/sentry/ISerializer;Lio/sentry/Session;)Lio/sentry/SentryEnvelopeItem; public static fun fromUserFeedback (Lio/sentry/ISerializer;Lio/sentry/UserFeedback;)Lio/sentry/SentryEnvelopeItem; @@ -2054,6 +2088,7 @@ public final class io/sentry/SentryItemType : java/lang/Enum, io/sentry/JsonSeri public static final field ReplayEvent Lio/sentry/SentryItemType; public static final field ReplayRecording Lio/sentry/SentryItemType; public static final field Session Lio/sentry/SentryItemType; + public static final field Statsd Lio/sentry/SentryItemType; public static final field Transaction Lio/sentry/SentryItemType; public static final field Unknown Lio/sentry/SentryItemType; public static final field UserFeedback Lio/sentry/SentryItemType; @@ -3322,6 +3357,138 @@ public abstract interface class io/sentry/internal/viewhierarchy/ViewHierarchyEx public abstract fun export (Lio/sentry/protocol/ViewHierarchyNode;Ljava/lang/Object;)Z } +public final class io/sentry/metrics/CodeLocations { + public fun (Ljava/util/Calendar;Ljava/util/Map;)V + public fun getDate ()Ljava/util/Calendar; + public fun getLocations ()Ljava/util/Map; +} + +public final class io/sentry/metrics/CounterMetric : io/sentry/metrics/Metric { + public fun (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;)V + public fun add (D)V + public fun getType ()Lio/sentry/metrics/MetricType; + public fun getValue ()D + public fun getValues ()Ljava/lang/Iterable; + public fun getWeight ()I +} + +public final class io/sentry/metrics/DistributionMetric : io/sentry/metrics/Metric { + public fun (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;)V + public fun add (D)V + public fun getType ()Lio/sentry/metrics/MetricType; + public fun getValues ()Ljava/lang/Iterable; + public fun getWeight ()I +} + +public final class io/sentry/metrics/EncodedMetrics { + public fun (Ljava/lang/String;)V + public fun getStatsd ()[B +} + +public final class io/sentry/metrics/GaugeMetric : io/sentry/metrics/Metric { + public fun (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;)V + public fun add (D)V + public fun getType ()Lio/sentry/metrics/MetricType; + public fun getValues ()Ljava/lang/Iterable; + public fun getWeight ()I +} + +public abstract interface class io/sentry/metrics/IMetricsHub { + public abstract fun captureMetrics (Lio/sentry/metrics/EncodedMetrics;)V +} + +public abstract class io/sentry/metrics/Metric { + public fun (Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;)V + public abstract fun add (D)V + public fun getKey ()Ljava/lang/String; + public fun getTags ()Ljava/util/Map; + public fun getTimeStamp ()Ljava/util/Calendar; + public abstract fun getType ()Lio/sentry/metrics/MetricType; + public fun getUnit ()Lio/sentry/MeasurementUnit; + public abstract fun getValues ()Ljava/lang/Iterable; + public abstract fun getWeight ()I +} + +public final class io/sentry/metrics/MetricHelper { + public static final field FLUSHER_SLEEP_TIME_MS I + public fun ()V + public static fun convertNanosTo (Lio/sentry/MeasurementUnit$Duration;J)D + public static fun encodeMetrics (JLjava/util/Collection;Ljava/lang/StringBuilder;)V + public static fun getCutoff ()Ljava/util/Calendar; + public static fun getDayBucketKey (Ljava/util/Calendar;)J + public static fun getFlushShiftMs ()D + public static fun getMetricBucketKey (Lio/sentry/metrics/MetricType;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;)Ljava/lang/String; + public static fun getTimeBucketKey (Ljava/util/Calendar;)J + public static fun sanitizeKey (Ljava/lang/String;)Ljava/lang/String; + public static fun sanitizeValue (Ljava/lang/String;)Ljava/lang/String; + public static fun toStatsdType (Lio/sentry/metrics/MetricType;)Ljava/lang/String; +} + +public final class io/sentry/metrics/MetricResourceIdentifier { + public fun (Lio/sentry/metrics/MetricType;Ljava/lang/String;Lio/sentry/MeasurementUnit;)V + public fun equals (Ljava/lang/Object;)Z + public fun getKey ()Ljava/lang/String; + public fun getMetricType ()Lio/sentry/metrics/MetricType; + public fun getUnit ()Lio/sentry/MeasurementUnit; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/sentry/metrics/MetricType : java/lang/Enum { + public static final field Counter Lio/sentry/metrics/MetricType; + public static final field Distribution Lio/sentry/metrics/MetricType; + public static final field Gauge Lio/sentry/metrics/MetricType; + public static final field Set Lio/sentry/metrics/MetricType; + public static fun valueOf (Ljava/lang/String;)Lio/sentry/metrics/MetricType; + public static fun values ()[Lio/sentry/metrics/MetricType; +} + +public final class io/sentry/metrics/MetricsApi { + public fun (Lio/sentry/IMetricAggregator;)V + public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V + public fun gauge (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V + public fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V + public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V + public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V + public fun timing (Ljava/lang/String;Lio/sentry/IMetricAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;Ljava/util/Calendar;I)V +} + +public final class io/sentry/metrics/NoopMetricAggregator : io/sentry/IMetricAggregator { + public fun ()V + public fun close ()V + public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V + public fun gauge (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V + public static fun getInstance ()Lio/sentry/metrics/NoopMetricAggregator; + public fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V + public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V + public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V + public fun timing (Ljava/lang/String;Lio/sentry/IMetricAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;Ljava/util/Calendar;I)V +} + +public final class io/sentry/metrics/SentryMetric : io/sentry/JsonSerializable { + public fun (Lio/sentry/metrics/Metric;)V + public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V +} + +public final class io/sentry/metrics/SentryMetric$JsonKeys { + public static final field EVENT_ID Ljava/lang/String; + public static final field NAME Ljava/lang/String; + public static final field TAGS Ljava/lang/String; + public static final field TIMESTAMP Ljava/lang/String; + public static final field TYPE Ljava/lang/String; + public static final field UNIT Ljava/lang/String; + public static final field VALUE Ljava/lang/String; + public fun ()V +} + +public final class io/sentry/metrics/SetMetric : io/sentry/metrics/Metric { + public fun (Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;)V + public fun add (D)V + public fun getType ()Lio/sentry/metrics/MetricType; + public fun getValues ()Ljava/lang/Iterable; + public fun getWeight ()I +} + public final class io/sentry/profilemeasurements/ProfileMeasurement : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public static final field ID_CPU_USAGE Ljava/lang/String; public static final field ID_FROZEN_FRAME_RENDERS Ljava/lang/String; diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index e7942722fc..28a759ab76 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -4,6 +4,9 @@ import io.sentry.clientreport.DiscardReason; import io.sentry.hints.SessionEndHint; import io.sentry.hints.SessionStartHint; +import io.sentry.metrics.EncodedMetrics; +import io.sentry.metrics.IMetricsHub; +import io.sentry.metrics.MetricsApi; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; @@ -24,7 +27,8 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public final class Hub implements IHub { +public final class Hub implements IHub, IMetricsHub { + private volatile @NotNull SentryId lastEventId; private final @NotNull SentryOptions options; private volatile boolean isEnabled; @@ -33,10 +37,11 @@ public final class Hub implements IHub { private final @NotNull Map, String>> throwableToSpan = Collections.synchronizedMap(new WeakHashMap<>()); private final @NotNull TransactionPerformanceCollector transactionPerformanceCollector; + private final @NotNull IMetricAggregator metricAggregator; + 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 } @@ -52,6 +57,9 @@ 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.metricAggregator = new MetricAggregator(this, options.getLogger()); + this.metricsApi = new MetricsApi(metricAggregator); } private Hub(final @NotNull SentryOptions options, final @NotNull StackItem rootStackItem) { @@ -283,6 +291,31 @@ public void captureUserFeedback(final @NotNull UserFeedback userFeedback) { } } + @Override + public void captureMetrics(final @NotNull EncodedMetrics metrics) { + if (!isEnabled()) { + options + .getLogger() + .log( + SentryLevel.WARNING, + "Instance is disabled and this 'captureMetrics' call is a no-op."); + } else { + final StackItem item = stack.peek(); + final SentryEnvelopeItem envelopeItem = SentryEnvelopeItem.fromMetrics(metrics); + + // TODO usually the envelope is assembled by the client + final SentryEnvelopeHeader envelopeHeader = + new SentryEnvelopeHeader( + new SentryId(), + options.getSdkVersion(), + item.getScope().getPropagationContext().traceContext()); + + final SentryEnvelope envelope = + new SentryEnvelope(envelopeHeader, Collections.singleton(envelopeItem)); + item.getClient().captureEnvelope(envelope); + } + } + @Override public void startSession() { if (!isEnabled()) { @@ -337,6 +370,13 @@ public void close() { .log(SentryLevel.WARNING, "Instance is disabled and this 'close' call is a no-op."); } else { try { + metricAggregator.close(); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "Error while closing metrics aggregator.", e); + } + try { + metricAggregator.close(); + for (Integration integration : options.getIntegrations()) { if (integration instanceof Closeable) { try { @@ -926,4 +966,9 @@ private IScope buildLocalScope( final StackItem item = stack.peek(); return item.getClient().getRateLimiter(); } + + @Override + public @NotNull MetricsApi getMetricsApi() { + return metricsApi; + } } diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index 68a9bdf11d..d4a6cbd8cf 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -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; @@ -267,4 +268,9 @@ public void reportFullyDisplayed() { public @Nullable RateLimiter getRateLimiter() { return Sentry.getCurrentHub().getRateLimiter(); } + + @Override + public @NotNull MetricsApi getMetricsApi() { + return Sentry.getCurrentHub().getMetricsApi(); + } } diff --git a/sentry/src/main/java/io/sentry/IHub.java b/sentry/src/main/java/io/sentry/IHub.java index 01431043f0..61d671fbf5 100644 --- a/sentry/src/main/java/io/sentry/IHub.java +++ b/sentry/src/main/java/io/sentry/IHub.java @@ -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; @@ -575,4 +576,8 @@ TransactionContext continueTrace( @ApiStatus.Internal @Nullable RateLimiter getRateLimiter(); + + @ApiStatus.Experimental + @NotNull + MetricsApi getMetricsApi(); } diff --git a/sentry/src/main/java/io/sentry/IMetricAggregator.java b/sentry/src/main/java/io/sentry/IMetricAggregator.java new file mode 100644 index 0000000000..26b0a4d2eb --- /dev/null +++ b/sentry/src/main/java/io/sentry/IMetricAggregator.java @@ -0,0 +1,127 @@ +package io.sentry; + +import java.io.Closeable; +import java.util.Calendar; +import java.util.Map; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public interface IMetricAggregator 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 timestamp 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 tags, + final @Nullable Calendar timestamp, + 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 timestamp 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 tags, + final @Nullable Calendar timestamp, + 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 timestamp 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 tags, + final @Nullable Calendar timestamp, + 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 timestamp 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 tags, + final @Nullable Calendar timestamp, + 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 timestamp 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 tags, + final @Nullable Calendar timestamp, + 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} + * @param tags Optional Tags to associate with the metric + * @param timestamp The time when the metric was emitted + * @param stackLevel Optional number of stacks levels to ignore when determining the code location + */ + void timing( + final @NotNull String key, + final @NotNull TimingCallback callback, + final @NotNull MeasurementUnit.Duration unit, + final @Nullable Map tags, + final @Nullable Calendar timestamp, + final int stackLevel); + + interface TimingCallback { + void run(); + } +} diff --git a/sentry/src/main/java/io/sentry/MetricAggregator.java b/sentry/src/main/java/io/sentry/MetricAggregator.java new file mode 100644 index 0000000000..7720f2228d --- /dev/null +++ b/sentry/src/main/java/io/sentry/MetricAggregator.java @@ -0,0 +1,251 @@ +package io.sentry; + +import io.sentry.metrics.CounterMetric; +import io.sentry.metrics.DistributionMetric; +import io.sentry.metrics.EncodedMetrics; +import io.sentry.metrics.GaugeMetric; +import io.sentry.metrics.IMetricsHub; +import io.sentry.metrics.Metric; +import io.sentry.metrics.MetricHelper; +import io.sentry.metrics.MetricType; +import io.sentry.metrics.SetMetric; +import java.io.Closeable; +import java.io.IOException; +import java.util.Calendar; +import java.util.HashMap; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Set; +import java.util.concurrent.ConcurrentSkipListMap; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class MetricAggregator implements IMetricAggregator, Runnable, Closeable { + + private final @NotNull IMetricsHub hub; + private final @NotNull ILogger logger; + + private @NotNull ISentryExecutorService executorService; + private volatile boolean isClosed = false; + + // The key for this dictionary is the Timestamp for the bucket, rounded down to the nearest + // RollupInSeconds... so it + // aggregates all of the metrics data for a particular time period. The Value is a dictionary for + // the metrics, + // each of which has a key that uniquely identifies it within the time period + private final NavigableMap> buckets = new ConcurrentSkipListMap<>(); + + public MetricAggregator(final @NotNull IMetricsHub hub, final @NotNull ILogger logger) { + this.hub = hub; + this.logger = logger; + this.executorService = NoOpSentryExecutorService.getInstance(); + } + + @Override + public void increment( + @NotNull String key, + double value, + @Nullable MeasurementUnit unit, + @Nullable Map tags, + @Nullable Calendar timestamp, + int stackLevel) { + add(MetricType.Counter, key, value, unit, tags, timestamp, stackLevel); + } + + @Override + public void gauge( + @NotNull String key, + double value, + @Nullable MeasurementUnit unit, + @Nullable Map tags, + @Nullable Calendar timestamp, + int stackLevel) { + add(MetricType.Gauge, key, value, unit, tags, timestamp, stackLevel); + } + + @Override + public void distribution( + @NotNull String key, + double value, + @Nullable MeasurementUnit unit, + @Nullable Map tags, + @Nullable Calendar timestamp, + int stackLevel) { + add(MetricType.Distribution, key, value, unit, tags, timestamp, stackLevel); + } + + @Override + public void set( + @NotNull String key, + int value, + @Nullable MeasurementUnit unit, + @Nullable Map tags, + @Nullable Calendar timestamp, + int stackLevel) { + add(MetricType.Set, key, value, unit, tags, timestamp, stackLevel); + } + + @Override + public void set( + @NotNull String key, + @NotNull String value, + @Nullable MeasurementUnit unit, + @Nullable Map tags, + @Nullable Calendar timestamp, + int stackLevel) { + // TODO consider using CR32 instead of hashCode + // see https://develop.sentry.dev/sdk/metrics/#sets + add(MetricType.Set, key, value.hashCode(), unit, tags, timestamp, stackLevel); + } + + @Override + public void timing( + @NotNull String key, + @NotNull TimingCallback callback, + @NotNull MeasurementUnit.Duration unit, + @Nullable Map tags, + @Nullable Calendar timestamp, + int stackLevel) { + final long start = System.nanoTime(); + try { + callback.run(); + } finally { + final long durationNanos = (System.nanoTime() - start); + final double value = MetricHelper.convertNanosTo(unit, durationNanos); + add(MetricType.Distribution, key, value, unit, tags, timestamp, stackLevel + 1); + } + } + + @SuppressWarnings({"FutureReturnValueIgnored", "UnusedVariable"}) + private void add( + final @NotNull MetricType type, + final @NotNull String key, + final double value, + @Nullable MeasurementUnit unit, + final @Nullable Map tags, + @Nullable Calendar timestamp, + final int stackLevel) { + if (timestamp == null) { + timestamp = Calendar.getInstance(); + } + + final @NotNull Metric metric; + switch (type) { + case Counter: + metric = new CounterMetric(key, value, unit, tags, timestamp); + break; + case Gauge: + metric = new GaugeMetric(key, value, unit, tags, timestamp); + break; + case Distribution: + metric = new DistributionMetric(key, value, unit, tags, timestamp); + break; + case Set: + metric = new SetMetric(key, unit, tags, timestamp); + //noinspection unchecked + metric.add((int) value); + break; + default: + throw new IllegalArgumentException("Unknown MetricType: " + type.name()); + } + + final long timeBucketKey = MetricHelper.getTimeBucketKey(timestamp); + final @NotNull Map timeBucket = getOrAddTimeBucket(timeBucketKey); + + final @NotNull String metricKey = MetricHelper.getMetricBucketKey(type, key, unit, tags); + synchronized (timeBucket) { + @Nullable Metric existingMetric = timeBucket.get(metricKey); + if (existingMetric != null) { + existingMetric.add(value); + } else { + timeBucket.put(metricKey, metric); + } + } + + // spin up read executor service the first time metrics are collected + if (executorService instanceof NoOpSentryExecutorService) { + synchronized (this) { + if (!isClosed && executorService instanceof NoOpSentryExecutorService) { + executorService = new SentryExecutorService(); + executorService.schedule(this, MetricHelper.FLUSHER_SLEEP_TIME_MS); + } + } + } + } + + public void flush(final boolean force) { + final @NotNull Set flushableBuckets = getFlushableBuckets(force); + if (flushableBuckets.isEmpty()) { + logger.log(SentryLevel.DEBUG, "Metrics: nothing to flush"); + return; + } + logger.log(SentryLevel.DEBUG, "Metrics: flushing " + flushableBuckets.size() + " buckets"); + + final @NotNull StringBuilder writer = new StringBuilder(); + for (long bucketKey : flushableBuckets) { + final @Nullable Map metrics = buckets.remove(bucketKey); + if (metrics != null) { + MetricHelper.encodeMetrics(bucketKey, metrics.values(), writer); + } + } + + if (writer.length() == 0) { + logger.log(SentryLevel.DEBUG, "Metrics: only empty buckets found"); + return; + } + + logger.log(SentryLevel.DEBUG, "Metrics: capturing metrics"); + final @NotNull EncodedMetrics encodedMetrics = new EncodedMetrics(writer.toString()); + hub.captureMetrics(encodedMetrics); + } + + @NotNull + public Set getFlushableBuckets(final boolean force) { + if (force) { + return buckets.keySet(); + } else { + // get all keys, including the cutoff key + final long cutoffKey = MetricHelper.getTimeBucketKey(MetricHelper.getCutoff()); + return buckets.headMap(cutoffKey, true).keySet(); + } + } + + @SuppressWarnings("Java8MapApi") + @NotNull + private Map getOrAddTimeBucket(final long bucketKey) { + @Nullable Map bucket = buckets.get(bucketKey); + if (bucket == null) { + // although buckets is thread safe, we still need to synchronize here to avoid overwriting + // buckets + synchronized (buckets) { + bucket = buckets.get(bucketKey); + if (bucket == null) { + bucket = new HashMap<>(); + buckets.put(bucketKey, bucket); + } + } + } + return bucket; + } + + @Override + public void close() throws IOException { + synchronized (this) { + this.isClosed = true; + executorService.close(0); + } + flush(true); + } + + @Override + @SuppressWarnings("FutureReturnValueIgnored") + public void run() { + flush(false); + + if (!isClosed) { + executorService.schedule(this, MetricHelper.FLUSHER_SLEEP_TIME_MS); + } + } +} diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index d186c69ca2..cc2d9d808b 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -1,5 +1,7 @@ package io.sentry; +import io.sentry.metrics.MetricsApi; +import io.sentry.metrics.NoopMetricAggregator; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; @@ -14,6 +16,7 @@ public final class NoOpHub implements IHub { private static final NoOpHub instance = new NoOpHub(); private final @NotNull SentryOptions emptyOptions = SentryOptions.empty(); + private final @NotNull MetricsApi metricsApi = new MetricsApi(NoopMetricAggregator.getInstance()); private NoOpHub() {} @@ -222,4 +225,9 @@ public void reportFullyDisplayed() {} public @Nullable RateLimiter getRateLimiter() { return null; } + + @Override + public @NotNull MetricsApi getMetricsApi() { + return metricsApi; + } } diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 0aff89c0d0..b8d33234e0 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -11,6 +11,7 @@ import io.sentry.internal.modules.ManifestModulesLoader; import io.sentry.internal.modules.NoOpModulesLoader; import io.sentry.internal.modules.ResourcesModulesLoader; +import io.sentry.metrics.MetricsApi; import io.sentry.protocol.SentryId; import io.sentry.protocol.User; import io.sentry.transport.NoOpEnvelopeCache; @@ -972,6 +973,13 @@ public static void reportFullDisplayed() { reportFullyDisplayed(); } + /** the metrics API for the current hub */ + @NotNull + @ApiStatus.Experimental + public static MetricsApi getMetricsApi() { + return getCurrentHub().getMetricsApi(); + } + /** * Configuration options callback * diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index acd6b36aa9..ea9773264e 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -6,6 +6,7 @@ import io.sentry.clientreport.ClientReport; import io.sentry.exception.SentryEnvelopeException; +import io.sentry.metrics.EncodedMetrics; import io.sentry.protocol.SentryTransaction; import io.sentry.util.JsonSerializationUtils; import io.sentry.util.Objects; @@ -184,7 +185,26 @@ public static SentryEnvelopeItem fromCheckIn( SentryEnvelopeItemHeader itemHeader = new SentryEnvelopeItemHeader( - SentryItemType.CheckIn, () -> cachedItem.getBytes().length, "application/json", null); + SentryItemType.Statsd, () -> cachedItem.getBytes().length, "application/json", null); + + // avoid method refs on Android due to some issues with older AGP setups + // noinspection Convert2MethodRef + return new SentryEnvelopeItem(itemHeader, () -> cachedItem.getBytes()); + } + + public static SentryEnvelopeItem fromMetrics(final @NotNull EncodedMetrics metrics) { + + final CachedItem cachedItem = + new CachedItem( + () -> { + // avoid method refs on Android due to some issues with older AGP setups + //noinspection Convert2MethodRef + return metrics.getStatsd(); + }); + + final @NotNull SentryEnvelopeItemHeader itemHeader = + new SentryEnvelopeItemHeader( + SentryItemType.Statsd, () -> cachedItem.getBytes().length, null, null); // avoid method refs on Android due to some issues with older AGP setups // noinspection Convert2MethodRef diff --git a/sentry/src/main/java/io/sentry/SentryItemType.java b/sentry/src/main/java/io/sentry/SentryItemType.java index c4535cb6a1..db299a12da 100644 --- a/sentry/src/main/java/io/sentry/SentryItemType.java +++ b/sentry/src/main/java/io/sentry/SentryItemType.java @@ -19,6 +19,7 @@ public enum SentryItemType implements JsonSerializable { ReplayEvent("replay_event"), ReplayRecording("replay_recording"), CheckIn("check_in"), + Statsd("statsd"), Unknown("__unknown__"); // DataCategory.Unknown private final String itemType; diff --git a/sentry/src/main/java/io/sentry/metrics/CodeLocations.java b/sentry/src/main/java/io/sentry/metrics/CodeLocations.java new file mode 100644 index 0000000000..c6d4d19003 --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/CodeLocations.java @@ -0,0 +1,32 @@ +package io.sentry.metrics; + +import io.sentry.protocol.SentryStackFrame; +import java.util.Calendar; +import java.util.Map; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** Represents a collection of code locations. */ +@ApiStatus.Internal +public final class CodeLocations { + + private final @NotNull Calendar date; + private final @NotNull Map locations; + + public CodeLocations( + final @NotNull Calendar date, + final @NotNull Map locations) { + this.date = date; + this.locations = locations; + } + + @NotNull + public Calendar getDate() { + return date; + } + + @NotNull + public Map getLocations() { + return locations; + } +} diff --git a/sentry/src/main/java/io/sentry/metrics/CounterMetric.java b/sentry/src/main/java/io/sentry/metrics/CounterMetric.java new file mode 100644 index 0000000000..3bea340a8d --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/CounterMetric.java @@ -0,0 +1,49 @@ +package io.sentry.metrics; + +import io.sentry.MeasurementUnit; +import java.util.Calendar; +import java.util.Collections; +import java.util.Map; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** Counters track a value that can only be incremented. */ +@ApiStatus.Internal +public final class CounterMetric extends Metric { + private double value; + + public CounterMetric( + final @NotNull String key, + final double value, + final @Nullable MeasurementUnit unit, + final @Nullable Map tags, + final @NotNull Calendar timestamp) { + super(key, unit, tags, timestamp); + this.value = value; + } + + public double getValue() { + return value; + } + + @Override + public void add(final double value) { + this.value += value; + } + + @Override + public MetricType getType() { + return MetricType.Counter; + } + + @Override + public int getWeight() { + return 1; + } + + @Override + public @NotNull Iterable getValues() { + return Collections.singletonList(value); + } +} diff --git a/sentry/src/main/java/io/sentry/metrics/DistributionMetric.java b/sentry/src/main/java/io/sentry/metrics/DistributionMetric.java new file mode 100644 index 0000000000..286cb51e72 --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/DistributionMetric.java @@ -0,0 +1,47 @@ +package io.sentry.metrics; + +import io.sentry.MeasurementUnit; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.Map; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class DistributionMetric extends Metric { + + private final List values; + + public DistributionMetric( + final @NotNull String key, + final double value, + final @Nullable MeasurementUnit unit, + final @Nullable Map tags, + final @NotNull Calendar timestamp) { + super(key, unit, tags, timestamp); + this.values = new ArrayList<>(); + this.values.add(value); + } + + @Override + public void add(final double value) { + values.add(value); + } + + @Override + public MetricType getType() { + return MetricType.Distribution; + } + + @Override + public int getWeight() { + return values.size(); + } + + @Override + public @NotNull Iterable getValues() { + return values; + } +} diff --git a/sentry/src/main/java/io/sentry/metrics/EncodedMetrics.java b/sentry/src/main/java/io/sentry/metrics/EncodedMetrics.java new file mode 100644 index 0000000000..4322c4b317 --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/EncodedMetrics.java @@ -0,0 +1,21 @@ +package io.sentry.metrics; + +import java.nio.charset.Charset; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public final class EncodedMetrics { + @SuppressWarnings("unused") + private static final Charset UTF8 = Charset.forName("UTF-8"); + + private final @NotNull String statsd; + + public EncodedMetrics(@NotNull String statsd) { + this.statsd = statsd; + } + + public byte[] getStatsd() { + return statsd.getBytes(UTF8); + } +} diff --git a/sentry/src/main/java/io/sentry/metrics/GaugeMetric.java b/sentry/src/main/java/io/sentry/metrics/GaugeMetric.java new file mode 100644 index 0000000000..b20a94494e --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/GaugeMetric.java @@ -0,0 +1,59 @@ +package io.sentry.metrics; + +import io.sentry.MeasurementUnit; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Map; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** Gauges track a value that can go up and down. */ +@ApiStatus.Internal +public final class GaugeMetric extends Metric { + + private double last; + private double min; + private double max; + private double sum; + private int count; + + public GaugeMetric( + final @NotNull String key, + final double value, + final @Nullable MeasurementUnit unit, + final @Nullable Map tags, + final @NotNull Calendar timestamp) { + super(key, unit, tags, timestamp); + + this.last = value; + this.min = value; + this.max = value; + this.sum = value; + this.count = 1; + } + + @Override + public void add(final double value) { + this.last = value; + min = Math.min(min, value); + max = Math.max(max, value); + sum += value; + count++; + } + + @Override + public MetricType getType() { + return MetricType.Gauge; + } + + @Override + public int getWeight() { + return 5; + } + + @Override + public @NotNull Iterable getValues() { + return Arrays.asList(last, min, max, sum, count); + } +} diff --git a/sentry/src/main/java/io/sentry/metrics/IMetricsHub.java b/sentry/src/main/java/io/sentry/metrics/IMetricsHub.java new file mode 100644 index 0000000000..da28c4c98a --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/IMetricsHub.java @@ -0,0 +1,20 @@ +package io.sentry.metrics; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public interface IMetricsHub { + /** Captures one or more metrics to be sent to Sentry. */ + void captureMetrics(final @NotNull EncodedMetrics metrics); + + /** Captures one or more to be sent to Sentry. */ + // void captureCodeLocations(final @NotNull CodeLocations codeLocations); + + /** + * Starts a child span for the current transaction or, if there is no active transaction, starts a + * new transaction. + */ + // @NotNull ISpan startSpan(final @NotNull String operation, final @NotNull String description); + +} diff --git a/sentry/src/main/java/io/sentry/metrics/Metric.java b/sentry/src/main/java/io/sentry/metrics/Metric.java new file mode 100644 index 0000000000..a40e45580d --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/Metric.java @@ -0,0 +1,66 @@ +package io.sentry.metrics; + +import io.sentry.MeasurementUnit; +import java.util.Calendar; +import java.util.Map; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** Base class for metric instruments */ +@ApiStatus.Internal +public abstract class Metric { + + private final @NotNull String key; + private final @Nullable MeasurementUnit unit; + private final @Nullable Map tags; + private final @NotNull Calendar timestamp; + + /** + * Creates a new instance of {@link Metric}. + * + * @param key The text key to be used to identify the metric + * @param unit An optional {@link MeasurementUnit} that describes the values being tracked + * @param tags An optional set of key/value paris that can be used to add dimensionality to + * metrics + * @param timestamp A time when the metric was emitted. + */ + public Metric( + @NotNull String key, + @Nullable MeasurementUnit unit, + @Nullable Map tags, + @NotNull Calendar timestamp) { + this.key = key; + this.unit = unit; + this.tags = tags; + this.timestamp = timestamp; + } + + /** Adds a value to the metric */ + public abstract void add(final double value); + + public abstract MetricType getType(); + + public abstract int getWeight(); + + @NotNull + public String getKey() { + return key; + } + + @Nullable + public MeasurementUnit getUnit() { + return unit; + } + + @Nullable + public Map getTags() { + return tags; + } + + public Calendar getTimeStamp() { + return timestamp; + } + + public abstract @NotNull Iterable getValues(); +} diff --git a/sentry/src/main/java/io/sentry/metrics/MetricHelper.java b/sentry/src/main/java/io/sentry/metrics/MetricHelper.java new file mode 100644 index 0000000000..4417a00ffe --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/MetricHelper.java @@ -0,0 +1,199 @@ +package io.sentry.metrics; + +import io.sentry.MeasurementUnit; +import java.util.Calendar; +import java.util.Collection; +import java.util.Map; +import java.util.Random; +import java.util.TimeZone; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class MetricHelper { + public static final int FLUSHER_SLEEP_TIME_MS = 5000; + private static final int ROLLUP_IN_SECONDS = 10; + + private static final String INVALID_KEY_CHARACTERS_PATTERN = "[^a-zA-Z0-9_/.-]+"; + private static final String INVALID_VALUE_CHARACTERS_PATTERN = "[^\\w\\d_:/@\\.\\{\\}\\[\\]$-]+"; + + private static final char TAGS_PAIR_DELIMITER = ','; // Delimiter between key-value pairs + private static final char TAGS_KEY_VALUE_DELIMITER = '='; // Delimiter between key and value + private static final char TAGS_ESCAPE_CHAR = '\\'; + + private static final double FLUSH_SHIFT_MS = + (long) (new Random().nextFloat() * (ROLLUP_IN_SECONDS * 1000f)); + + public static long getDayBucketKey(final @NotNull Calendar timestamp) { + final Calendar utc = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + utc.set(Calendar.YEAR, timestamp.get(Calendar.YEAR)); + utc.set(Calendar.MONTH, timestamp.get(Calendar.MONTH)); + utc.set(Calendar.DAY_OF_MONTH, timestamp.get(Calendar.DAY_OF_MONTH)); + + return utc.getTimeInMillis() / 1000; + } + + public static long getTimeBucketKey(final @NotNull Calendar timestamp) { + final long seconds = timestamp.getTimeInMillis() / 1000; + return (seconds / ROLLUP_IN_SECONDS) * ROLLUP_IN_SECONDS; + } + + public static double getFlushShiftMs() { + return FLUSH_SHIFT_MS; + } + + public static Calendar getCutoff() { + final Calendar cutOff = Calendar.getInstance(); + cutOff.add(Calendar.SECOND, -ROLLUP_IN_SECONDS); + cutOff.add(Calendar.MILLISECOND, (int) -FLUSH_SHIFT_MS); + return cutOff; + } + + public static @NotNull String sanitizeKey(final @NotNull String input) { + return input.replaceAll(INVALID_KEY_CHARACTERS_PATTERN, "_"); + } + + public static String sanitizeValue(final @NotNull String input) { + return input.replaceAll(INVALID_VALUE_CHARACTERS_PATTERN, "_"); + } + + public static @NotNull String toStatsdType(final @NotNull MetricType type) { + switch (type) { + case Counter: + return "c"; + case Gauge: + return "g"; + case Distribution: + return "d"; + case Set: + return "s"; + default: + throw new IllegalArgumentException("Invalid Metric Type: " + type.name()); + } + } + + @NotNull + public static String getMetricBucketKey( + final @NotNull MetricType type, + final @NotNull String metricKey, + final @Nullable MeasurementUnit unit, + final @Nullable Map tags) { + final @NotNull String typePrefix = toStatsdType(type); + final @NotNull String serializedTags = GetTagsKey(tags); + + return String.format("%s_%s_%s_%s", typePrefix, metricKey, unit, serializedTags); + } + + private static String GetTagsKey(final @Nullable Map tags) { + if (tags == null || tags.isEmpty()) { + return ""; + } + + final @NotNull StringBuilder builder = new StringBuilder(); + for (Map.Entry tag : tags.entrySet()) { + + // Escape delimiters in key and value + final @NotNull String key = escapeString(tag.getKey()); + final @NotNull String value = escapeString(tag.getValue()); + + if (builder.length() > 0) { + builder.append(TAGS_PAIR_DELIMITER); + } + + builder.append(key).append(TAGS_KEY_VALUE_DELIMITER).append(value); + } + + return builder.toString(); + } + + @NotNull + private static String escapeString(final @NotNull String input) { + final StringBuilder escapedString = new StringBuilder(input.length()); + + for (int idx = 0; idx < input.length(); idx++) { + final char ch = input.charAt(idx); + + if (ch == TAGS_PAIR_DELIMITER || ch == TAGS_KEY_VALUE_DELIMITER) { + escapedString.append(TAGS_ESCAPE_CHAR); // Prefix with escape character + } + escapedString.append(ch); + } + + return escapedString.toString(); + } + + public static double convertNanosTo( + final @NotNull MeasurementUnit.Duration unit, final long durationNanos) { + switch (unit) { + case NANOSECOND: + return durationNanos; + case MICROSECOND: + return (double) durationNanos / 1000.0d; + case MILLISECOND: + return (double) durationNanos / 1000000.0d; + case SECOND: + return (double) durationNanos / 1000000000.0d; + case MINUTE: + return (double) durationNanos / 60000000000.0d; + case HOUR: + return (double) durationNanos / 3600000000000.0d; + case DAY: + return (double) durationNanos / 86400000000000.0d; + case WEEK: + return (double) durationNanos / 86400000000000.0d / 7.0d; + default: + throw new IllegalArgumentException("Unknown Duration unit: " + unit.name()); + } + } + + /** + * Encodes the metrics + * + * @param timestamp The bucket time the metrics belong to, in second resolution + * @param metrics The metrics to encode + * @param writer The writer to encode the metrics into + */ + public static void encodeMetrics( + final long timestamp, + final @NotNull Collection metrics, + final @NotNull StringBuilder writer) { + for (Metric metric : metrics) { + writer.append(sanitizeKey(metric.getKey())); + writer.append("@"); + + final MeasurementUnit unit = metric.getUnit(); + final String unitName = (unit != null) ? unit.apiName() : MeasurementUnit.NONE; + writer.append(unitName); + + for (final @NotNull Object value : metric.getValues()) { + writer.append(":"); + writer.append(value); + } + + writer.append("|"); + writer.append(toStatsdType(metric.getType())); + + final @Nullable Map tags = metric.getTags(); + if (tags != null) { + writer.append("|#"); + boolean first = true; + for (final @NotNull Map.Entry tag : tags.entrySet()) { + final @NotNull String tagKey = sanitizeKey(tag.getKey()); + if (first) { + first = false; + } else { + writer.append(","); + } + writer.append(tagKey); + writer.append(":"); + writer.append(sanitizeValue(tag.getValue())); + } + } + + writer.append("|T"); + writer.append(timestamp); + writer.append("\n"); + } + } +} diff --git a/sentry/src/main/java/io/sentry/metrics/MetricResourceIdentifier.java b/sentry/src/main/java/io/sentry/metrics/MetricResourceIdentifier.java new file mode 100644 index 0000000000..097f378720 --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/MetricResourceIdentifier.java @@ -0,0 +1,64 @@ +package io.sentry.metrics; + +import io.sentry.MeasurementUnit; +import io.sentry.util.Objects; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Uniquely identifies a metric resource. Used for caching the {@link CodeLocations} for a given + * metric. + */ +@ApiStatus.Internal +public final class MetricResourceIdentifier { + private final @NotNull MetricType metricType; + private final @NotNull String key; + private final @Nullable MeasurementUnit unit; + + public MetricResourceIdentifier( + final @NotNull MetricType metricType, + final @NotNull String key, + final @Nullable MeasurementUnit unit) { + this.metricType = metricType; + this.key = key; + this.unit = unit; + } + + @NotNull + public MetricType getMetricType() { + return metricType; + } + + @NotNull + public String getKey() { + return key; + } + + @Nullable + public MeasurementUnit getUnit() { + return unit; + } + + /** Returns a string representation of the metric resource identifier. */ + @Override + public String toString() { + return String.format( + "%s:%s@%s", MetricHelper.toStatsdType(metricType), MetricHelper.sanitizeKey(key), unit); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final @NotNull MetricResourceIdentifier that = (MetricResourceIdentifier) o; + return metricType == that.metricType + && Objects.equals(key, that.key) + && Objects.equals(unit, that.unit); + } + + @Override + public int hashCode() { + return Objects.hash(metricType, key, unit); + } +} diff --git a/sentry/src/main/java/io/sentry/metrics/MetricType.java b/sentry/src/main/java/io/sentry/metrics/MetricType.java new file mode 100644 index 0000000000..56fb10ee1e --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/MetricType.java @@ -0,0 +1,12 @@ +package io.sentry.metrics; + +import org.jetbrains.annotations.ApiStatus; + +/** The metric instrument type */ +@ApiStatus.Internal +public enum MetricType { + Counter, + Gauge, + Distribution, + Set +} diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java new file mode 100644 index 0000000000..48f92abd17 --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java @@ -0,0 +1,143 @@ +package io.sentry.metrics; + +import io.sentry.IMetricAggregator; +import io.sentry.MeasurementUnit; +import java.util.Calendar; +import java.util.Map; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +// TODO: add tons of method overloads to make it delightful to use +public final class MetricsApi { + + private final @NotNull IMetricAggregator aggregator; + + public MetricsApi(final @NotNull IMetricAggregator aggregator) { + this.aggregator = aggregator; + } + + /** + * 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 timestamp 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 + */ + public void increment( + final @NotNull String key, + final double value, + final @Nullable MeasurementUnit unit, + final @Nullable Map tags, + final @Nullable Calendar timestamp, + final int stackLevel) { + aggregator.increment(key, value, unit, tags, timestamp, 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 timestamp 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 + */ + public void gauge( + final @NotNull String key, + final double value, + final @Nullable MeasurementUnit unit, + final @Nullable Map tags, + final @Nullable Calendar timestamp, + final int stackLevel) { + aggregator.gauge(key, value, unit, tags, timestamp, 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 timestamp 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 + */ + public void distribution( + final @NotNull String key, + final double value, + final @Nullable MeasurementUnit unit, + final @Nullable Map tags, + final @Nullable Calendar timestamp, + final int stackLevel) { + aggregator.distribution(key, value, unit, tags, timestamp, 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 timestamp 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 + */ + public void set( + final @NotNull String key, + final int value, + final @Nullable MeasurementUnit unit, + final @Nullable Map tags, + final @Nullable Calendar timestamp, + final int stackLevel) { + aggregator.set(key, value, unit, tags, timestamp, 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 timestamp 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 + */ + public void set( + final @NotNull String key, + final @NotNull String value, + final @Nullable MeasurementUnit unit, + final @Nullable Map tags, + final @Nullable Calendar timestamp, + final int stackLevel) { + aggregator.set(key, value, unit, tags, timestamp, 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} + * @param tags Optional Tags to associate with the metric + * @param timestamp The time when the metric was emitted + * @param stackLevel Optional number of stacks levels to ignore when determining the code location + */ + public void timing( + final @NotNull String key, + final @NotNull IMetricAggregator.TimingCallback callback, + final @NotNull MeasurementUnit.Duration unit, + final @Nullable Map tags, + final @Nullable Calendar timestamp, + final int stackLevel) { + aggregator.timing(key, callback, unit, tags, timestamp, stackLevel); + } +} diff --git a/sentry/src/main/java/io/sentry/metrics/NoopMetricAggregator.java b/sentry/src/main/java/io/sentry/metrics/NoopMetricAggregator.java new file mode 100644 index 0000000000..99a962bc09 --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/NoopMetricAggregator.java @@ -0,0 +1,77 @@ +package io.sentry.metrics; + +import io.sentry.IMetricAggregator; +import io.sentry.MeasurementUnit; +import java.io.IOException; +import java.util.Calendar; +import java.util.Map; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class NoopMetricAggregator implements IMetricAggregator { + + private static final NoopMetricAggregator instance = new NoopMetricAggregator(); + + public static NoopMetricAggregator getInstance() { + return instance; + } + + @Override + public void increment( + @NotNull String key, + double value, + @Nullable MeasurementUnit unit, + @Nullable Map tags, + @Nullable Calendar timestamp, + int stackLevel) {} + + @Override + public void gauge( + @NotNull String key, + double value, + @Nullable MeasurementUnit unit, + @Nullable Map tags, + @Nullable Calendar timestamp, + int stackLevel) {} + + @Override + public void distribution( + @NotNull String key, + double value, + @Nullable MeasurementUnit unit, + @Nullable Map tags, + @Nullable Calendar timestamp, + int stackLevel) {} + + @Override + public void set( + @NotNull String key, + int value, + @Nullable MeasurementUnit unit, + @Nullable Map tags, + @Nullable Calendar timestamp, + int stackLevel) {} + + @Override + public void set( + @NotNull String key, + @NotNull String value, + @Nullable MeasurementUnit unit, + @Nullable Map tags, + @Nullable Calendar timestamp, + int stackLevel) {} + + @Override + public void timing( + @NotNull String key, + @NotNull TimingCallback callback, + MeasurementUnit.@NotNull Duration unit, + @Nullable Map tags, + @Nullable Calendar timestamp, + int stackLevel) {} + + @Override + public void close() throws IOException {} +} diff --git a/sentry/src/main/java/io/sentry/metrics/SentryMetric.java b/sentry/src/main/java/io/sentry/metrics/SentryMetric.java new file mode 100644 index 0000000000..cc94d4c8eb --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/SentryMetric.java @@ -0,0 +1,78 @@ +package io.sentry.metrics; + +import io.sentry.ILogger; +import io.sentry.JsonSerializable; +import io.sentry.MeasurementUnit; +import io.sentry.ObjectWriter; +import io.sentry.protocol.SentryId; +import java.io.IOException; +import java.util.Calendar; +import java.util.Map; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +// TODO actually needed? we seem to send them in statsd format anyway +@ApiStatus.Internal +public final class SentryMetric implements JsonSerializable { + + private final Iterable values; + + public static final class JsonKeys { + public static final String TYPE = "type"; + public static final String EVENT_ID = "event_id"; + public static final String NAME = "name"; + public static final String TIMESTAMP = "timestamp"; + public static final String UNIT = "unit"; + public static final String TAGS = "tags"; + public static final String VALUE = "value"; + } + + private final @NotNull MetricType type; + private final @NotNull SentryId eventId; + private final @NotNull String key; + private final @Nullable MeasurementUnit unit; + private final @Nullable Map tags; + private final @NotNull Calendar timestamp; + + public SentryMetric(@NotNull Metric metric) { + this.eventId = new SentryId(); + + this.type = metric.getType(); + this.key = metric.getKey(); + this.unit = metric.getUnit(); + this.tags = metric.getTags(); + this.timestamp = metric.getTimeStamp(); + this.values = metric.getValues(); + } + + @Override + public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) throws IOException { + + writer.beginObject(); + writer.name(JsonKeys.TYPE).value(MetricHelper.toStatsdType(type)); + writer.name(JsonKeys.EVENT_ID).value(logger, eventId); + writer.name(JsonKeys.NAME).value(key); + writer.name(JsonKeys.TIMESTAMP).value(timestamp.getTimeInMillis() / 1000.0d); + if (unit != null) { + writer.name(JsonKeys.UNIT).value(unit.apiName()); + } + if (tags != null) { + writer.name(JsonKeys.TAGS); + writer.beginObject(); + for (final @NotNull Map.Entry entry : tags.entrySet()) { + writer.name(entry.getKey()).value(entry.getValue()); + } + writer.endObject(); + } + + writer.name(JsonKeys.VALUE); + writer.beginArray(); + for (final Object value : values) { + writer.value(logger, value); + } + writer.endArray(); + + writer.endObject(); + } +} diff --git a/sentry/src/main/java/io/sentry/metrics/SetMetric.java b/sentry/src/main/java/io/sentry/metrics/SetMetric.java new file mode 100644 index 0000000000..6875b9ba83 --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/SetMetric.java @@ -0,0 +1,46 @@ +package io.sentry.metrics; + +import io.sentry.MeasurementUnit; +import java.util.Calendar; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** Sets track a set of values on which you can perform aggregations such as count_unique. */ +@ApiStatus.Internal +public final class SetMetric extends Metric { + + private final @NotNull Set values; + + public SetMetric( + final @NotNull String key, + final @Nullable MeasurementUnit unit, + final @Nullable Map tags, + final @NotNull Calendar timestamp) { + super(key, unit, tags, timestamp); + this.values = new HashSet<>(); + } + + @Override + public void add(final double value) { + values.add((int) value); + } + + @Override + public MetricType getType() { + return MetricType.Set; + } + + @Override + public int getWeight() { + return values.size(); + } + + @Override + public @NotNull Iterable getValues() { + return values; + } +} diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 8fab30790f..e51b9e985e 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -549,7 +549,7 @@ class SentryClientTest { assertEquals(1, actual.items.count()) val item = actual.items.first() - assertEquals(SentryItemType.CheckIn, item.header.type) + assertEquals(SentryItemType.Statsd, item.header.type) assertEquals("application/json", item.header.contentType) assertEnvelopeItemDataForCheckIn(item) @@ -573,7 +573,7 @@ class SentryClientTest { assertEquals(1, actual.items.count()) val item = actual.items.first() - assertEquals(SentryItemType.CheckIn, item.header.type) + assertEquals(SentryItemType.Statsd, item.header.type) assertEquals("application/json", item.header.contentType) assertEnvelopeItemDataForCheckIn(item) diff --git a/sentry/src/test/java/io/sentry/metrics/CounterMetricTest.kt b/sentry/src/test/java/io/sentry/metrics/CounterMetricTest.kt new file mode 100644 index 0000000000..d94a027a21 --- /dev/null +++ b/sentry/src/test/java/io/sentry/metrics/CounterMetricTest.kt @@ -0,0 +1,84 @@ +package io.sentry.metrics + +import java.util.Calendar +import kotlin.test.Test +import kotlin.test.assertEquals + +class CounterMetricTest { + + @Test + fun add() { + val metric = CounterMetric( + "test", + 1.0, + null, + mapOf( + "tag1" to "value1", + "tag2" to "value2" + ), + Calendar.getInstance() + ) + assertEquals(1.0, metric.value) + + metric.add(2.0) + assertEquals(3.0, metric.value) + + // TODO should we allow negative values? + // TODO should we do any bounds checks? + metric.add(-3.0) + assertEquals(0.0, metric.value) + } + + @Test + fun type() { + val metric = CounterMetric( + "test", + 1.0, + null, + mapOf( + "tag1" to "value1", + "tag2" to "value2" + ), + Calendar.getInstance() + ) + assertEquals(MetricType.Counter, metric.type) + } + + @Test + fun weight() { + val metric = CounterMetric( + "test", + 1.0, + null, + mapOf( + "tag1" to "value1", + "tag2" to "value2" + ), + Calendar.getInstance() + ) + assertEquals(1, metric.weight) + } + + @Test + fun values() { + val metric = CounterMetric( + "test", + 1.0, + null, + mapOf( + "tag1" to "value1", + "tag2" to "value2" + ), + Calendar.getInstance() + ) + + val values0 = metric.values.toList() + assertEquals(1, values0.size) + assertEquals(1.0, values0[0] as Double) + + metric.add(1.0) + val values1 = metric.values.toList() + assertEquals(1, values1.size) + assertEquals(2.0, values1[0] as Double) + } +} diff --git a/sentry/src/test/java/io/sentry/metrics/DistributionMetricTest.kt b/sentry/src/test/java/io/sentry/metrics/DistributionMetricTest.kt new file mode 100644 index 0000000000..70677f0398 --- /dev/null +++ b/sentry/src/test/java/io/sentry/metrics/DistributionMetricTest.kt @@ -0,0 +1,67 @@ +package io.sentry.metrics + +import java.util.Calendar +import kotlin.test.Test +import kotlin.test.assertEquals + +class DistributionMetricTest { + + @Test + fun add() { + val metric = DistributionMetric( + "test", + 1.0, + null, + null, + Calendar.getInstance() + ) + assertEquals(listOf(1.0), metric.values.toList()) + + metric.add(1.0) + metric.add(2.0) + assertEquals(listOf(1.0, 1.0, 2.0), metric.values.toList()) + } + + @Test + fun type() { + val metric = DistributionMetric( + "test", + 1.0, + null, + null, + Calendar.getInstance() + ) + assertEquals(MetricType.Distribution, metric.type) + } + + @Test + fun weight() { + val metric = DistributionMetric( + "test", + 1.0, + null, + null, + Calendar.getInstance() + ) + assertEquals(1, metric.weight) + + metric.add(2.0) + assertEquals(2, metric.weight) + } + + @Test + fun values() { + val metric = DistributionMetric( + "test", + 1.0, + null, + null, + Calendar.getInstance() + ) + metric.add(2.0) + + val values = metric.values.toList() + assertEquals(2, values.size) + assertEquals(listOf(1.0, 2.0), values) + } +} diff --git a/sentry/src/test/java/io/sentry/metrics/GaugeMetricTest.kt b/sentry/src/test/java/io/sentry/metrics/GaugeMetricTest.kt new file mode 100644 index 0000000000..a83759fe7f --- /dev/null +++ b/sentry/src/test/java/io/sentry/metrics/GaugeMetricTest.kt @@ -0,0 +1,73 @@ +package io.sentry.metrics + +import java.util.Calendar +import kotlin.test.Test +import kotlin.test.assertEquals + +class GaugeMetricTest { + + @Test + fun add() { + val metric = GaugeMetric( + "test", + 1.0, + null, + null, + Calendar.getInstance() + ) + assertEquals( + listOf( + 1.0, + 1.0, + 1.0, + 1.0, + 1 + ), + metric.values.toList() + ) + + metric.add(5.0) + metric.add(4.0) + metric.add(3.0) + metric.add(2.0) + metric.add(1.0) + assertEquals( + listOf( + 1.0, // last + 1.0, // min + 5.0, // max + 16.0, // sum + 6 // count + ), + metric.values.toList() + ) + } + + @Test + fun type() { + val metric = GaugeMetric( + "test", + 1.0, + null, + null, + Calendar.getInstance() + ) + assertEquals(MetricType.Gauge, metric.type) + } + + @Test + fun weight() { + val metric = GaugeMetric( + "test", + 1.0, + null, + null, + Calendar.getInstance() + ) + assertEquals(5, metric.weight) + + // even when values are added, the weight is still 5 + metric.add(2.0) + assertEquals(5, metric.weight) + } +} diff --git a/sentry/src/test/java/io/sentry/metrics/MetricHelperTest.kt b/sentry/src/test/java/io/sentry/metrics/MetricHelperTest.kt new file mode 100644 index 0000000000..e42e8c29f2 --- /dev/null +++ b/sentry/src/test/java/io/sentry/metrics/MetricHelperTest.kt @@ -0,0 +1,70 @@ +package io.sentry.metrics + +import java.util.Calendar +import kotlin.test.Test +import kotlin.test.assertEquals + +class MetricHelperTest { + + @Test + fun sanitizeKey() { + assertEquals("foo-bar", MetricHelper.sanitizeKey("foo-bar")) + assertEquals("foo_bar", MetricHelper.sanitizeKey("foo\$\$\$bar")) + assertEquals("fo_-bar", MetricHelper.sanitizeKey("foö-bar")) + } + + @Test + fun sanitizeValue() { + assertEquals("_\$foo", MetricHelper.sanitizeValue("%\$foo")) + assertEquals("blah{}", MetricHelper.sanitizeValue("blah{}")) + assertEquals("sn_wm_n", MetricHelper.sanitizeValue("snöwmän")) + } + + @Test + fun getTimeBucketKey() { + assertEquals( + 10, + MetricHelper.getTimeBucketKey( + Calendar.getInstance().apply { + timeInMillis = 10_000 + } + ) + ) + + assertEquals( + 10, + MetricHelper.getTimeBucketKey( + Calendar.getInstance().apply { + timeInMillis = 10_001 + } + ) + ) + + assertEquals( + 20, + MetricHelper.getTimeBucketKey( + Calendar.getInstance().apply { + timeInMillis = 20_000 + } + ) + ) + + assertEquals( + 20, + MetricHelper.getTimeBucketKey( + Calendar.getInstance().apply { + timeInMillis = 29_999 + } + ) + ) + + assertEquals( + 30, + MetricHelper.getTimeBucketKey( + Calendar.getInstance().apply { + timeInMillis = 30_000 + } + ) + ) + } +} diff --git a/sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt b/sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt new file mode 100644 index 0000000000..3f339b39dd --- /dev/null +++ b/sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt @@ -0,0 +1,69 @@ +package io.sentry.metrics + +import java.util.Calendar +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class SetMetricTest { + + @Test + fun add() { + val metric = SetMetric( + "test", + null, + null, + Calendar.getInstance() + ) + assertTrue(metric.values.toList().isEmpty()) + + metric.add(1.0) + metric.add(2.0) + metric.add(3.0) + + assertEquals(3, metric.values.toList().size) + + // when an already existing item is added + // size stays the same + metric.add(3.0) + assertEquals(3, metric.values.toList().size) + } + + @Test + fun type() { + val metric = SetMetric( + "test", + null, + null, + Calendar.getInstance() + ) + assertEquals(MetricType.Set, metric.type) + } + + @Test + fun weight() { + val metric = SetMetric( + "test", + null, + null, + Calendar.getInstance() + ) + assertEquals(0, metric.weight) + + metric.add(1.0) + metric.add(2.0) + metric.add(3.0) + metric.add(3.0) + + // weight should be the number of distinct items + assertEquals(3, metric.weight) + } + + @Test + fun toStatsdType() { + assertEquals("c", MetricHelper.toStatsdType(MetricType.Counter)) + assertEquals("g", MetricHelper.toStatsdType(MetricType.Gauge)) + assertEquals("s", MetricHelper.toStatsdType(MetricType.Set)) + assertEquals("d", MetricHelper.toStatsdType(MetricType.Distribution)) + } +} From 1d9aca377cae48e7da8e4ec148e42336d0deda27 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 14 Feb 2024 10:33:47 +0100 Subject: [PATCH 02/26] Fix typos, make executorService volatile --- sentry/src/main/java/io/sentry/MetricAggregator.java | 6 +++--- sentry/src/main/java/io/sentry/metrics/Metric.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry/src/main/java/io/sentry/MetricAggregator.java b/sentry/src/main/java/io/sentry/MetricAggregator.java index 7720f2228d..a12cea6ac7 100644 --- a/sentry/src/main/java/io/sentry/MetricAggregator.java +++ b/sentry/src/main/java/io/sentry/MetricAggregator.java @@ -27,7 +27,7 @@ public final class MetricAggregator implements IMetricAggregator, Runnable, Clos private final @NotNull IMetricsHub hub; private final @NotNull ILogger logger; - private @NotNull ISentryExecutorService executorService; + private volatile @NotNull ISentryExecutorService executorService; private volatile boolean isClosed = false; // The key for this dictionary is the Timestamp for the bucket, rounded down to the nearest @@ -164,8 +164,8 @@ private void add( } } - // spin up read executor service the first time metrics are collected - if (executorService instanceof NoOpSentryExecutorService) { + // spin up real executor service the first time metrics are collected + if (!isClosed && executorService instanceof NoOpSentryExecutorService) { synchronized (this) { if (!isClosed && executorService instanceof NoOpSentryExecutorService) { executorService = new SentryExecutorService(); diff --git a/sentry/src/main/java/io/sentry/metrics/Metric.java b/sentry/src/main/java/io/sentry/metrics/Metric.java index a40e45580d..2eb9c833af 100644 --- a/sentry/src/main/java/io/sentry/metrics/Metric.java +++ b/sentry/src/main/java/io/sentry/metrics/Metric.java @@ -21,7 +21,7 @@ public abstract class Metric { * * @param key The text key to be used to identify the metric * @param unit An optional {@link MeasurementUnit} that describes the values being tracked - * @param tags An optional set of key/value paris that can be used to add dimensionality to + * @param tags An optional set of key/value pairs that can be used to add dimensionality to * metrics * @param timestamp A time when the metric was emitted. */ From 98b01f078bfe79d36bf194cb0ce18602f9405f10 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 14 Feb 2024 13:36:02 +0100 Subject: [PATCH 03/26] Move from Calendar to long timestamps, add hooks for testing --- sentry/api/sentry.api | 70 ++++++++++--------- .../java/io/sentry/IMetricAggregator.java | 37 +++++----- .../main/java/io/sentry/MetricAggregator.java | 58 +++++++++------ .../java/io/sentry/metrics/CounterMetric.java | 3 +- .../io/sentry/metrics/DistributionMetric.java | 3 +- .../java/io/sentry/metrics/GaugeMetric.java | 3 +- .../main/java/io/sentry/metrics/Metric.java | 14 ++-- .../java/io/sentry/metrics/MetricHelper.java | 13 ++-- .../java/io/sentry/metrics/MetricsApi.java | 47 ++++++------- .../sentry/metrics/NoopMetricAggregator.java | 18 +++-- .../java/io/sentry/metrics/SentryMetric.java | 7 +- .../java/io/sentry/metrics/SetMetric.java | 3 +- .../java/io/sentry/MetricAggregatorTest.kt | 64 +++++++++++++++++ .../io/sentry/metrics/CounterMetricTest.kt | 9 ++- .../sentry/metrics/DistributionMetricTest.kt | 9 ++- .../java/io/sentry/metrics/GaugeMetricTest.kt | 7 +- .../io/sentry/metrics/MetricHelperTest.kt | 31 ++------ .../java/io/sentry/metrics/SetMetricTest.kt | 7 +- 18 files changed, 229 insertions(+), 174 deletions(-) create mode 100644 sentry/src/test/java/io/sentry/MetricAggregatorTest.kt diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 5a23d7e84b..05ef4d8e9f 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -619,12 +619,13 @@ public abstract interface class io/sentry/IMemoryCollector { } public abstract interface class io/sentry/IMetricAggregator : java/io/Closeable { - public abstract fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V - public abstract fun gauge (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V - public abstract fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V - public abstract fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V - public abstract fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V - public abstract fun timing (Ljava/lang/String;Lio/sentry/IMetricAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;Ljava/util/Calendar;I)V + public abstract fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V + public abstract fun flush (Z)V + public abstract fun gauge (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V + public abstract fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V + public abstract fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;JI)V + public abstract fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;JI)V + public abstract fun timing (Ljava/lang/String;Lio/sentry/IMetricAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;JI)V } public abstract interface class io/sentry/IMetricAggregator$TimingCallback { @@ -1021,15 +1022,19 @@ public final class io/sentry/MemoryCollectionData { public final class io/sentry/MetricAggregator : io/sentry/IMetricAggregator, java/io/Closeable, java/lang/Runnable { public fun (Lio/sentry/metrics/IMetricsHub;Lio/sentry/ILogger;)V public fun close ()V - public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V + public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public fun flush (Z)V - public fun gauge (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V + public fun gauge (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public fun getFlushableBuckets (Z)Ljava/util/Set; - public fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V + public fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public fun run ()V - public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V - public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V - public fun timing (Ljava/lang/String;Lio/sentry/IMetricAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;Ljava/util/Calendar;I)V + public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;JI)V + public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;JI)V + public fun timing (Ljava/lang/String;Lio/sentry/IMetricAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;JI)V +} + +public abstract interface class io/sentry/MetricAggregator$TimeProvider { + public abstract fun getTimeMillis ()J } public final class io/sentry/MonitorConfig : io/sentry/JsonSerializable, io/sentry/JsonUnknown { @@ -3364,7 +3369,7 @@ public final class io/sentry/metrics/CodeLocations { } public final class io/sentry/metrics/CounterMetric : io/sentry/metrics/Metric { - public fun (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;)V + public fun (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun add (D)V public fun getType ()Lio/sentry/metrics/MetricType; public fun getValue ()D @@ -3373,7 +3378,7 @@ public final class io/sentry/metrics/CounterMetric : io/sentry/metrics/Metric { } public final class io/sentry/metrics/DistributionMetric : io/sentry/metrics/Metric { - public fun (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;)V + public fun (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun add (D)V public fun getType ()Lio/sentry/metrics/MetricType; public fun getValues ()Ljava/lang/Iterable; @@ -3386,7 +3391,7 @@ public final class io/sentry/metrics/EncodedMetrics { } public final class io/sentry/metrics/GaugeMetric : io/sentry/metrics/Metric { - public fun (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;)V + public fun (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun add (D)V public fun getType ()Lio/sentry/metrics/MetricType; public fun getValues ()Ljava/lang/Iterable; @@ -3398,11 +3403,11 @@ public abstract interface class io/sentry/metrics/IMetricsHub { } public abstract class io/sentry/metrics/Metric { - public fun (Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;)V + public fun (Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public abstract fun add (D)V public fun getKey ()Ljava/lang/String; public fun getTags ()Ljava/util/Map; - public fun getTimeStamp ()Ljava/util/Calendar; + public fun getTimeStampMs ()Ljava/lang/Long; public abstract fun getType ()Lio/sentry/metrics/MetricType; public fun getUnit ()Lio/sentry/MeasurementUnit; public abstract fun getValues ()Ljava/lang/Iterable; @@ -3414,11 +3419,11 @@ public final class io/sentry/metrics/MetricHelper { public fun ()V public static fun convertNanosTo (Lio/sentry/MeasurementUnit$Duration;J)D public static fun encodeMetrics (JLjava/util/Collection;Ljava/lang/StringBuilder;)V - public static fun getCutoff ()Ljava/util/Calendar; + public static fun getCutoffTimestampMs (J)J public static fun getDayBucketKey (Ljava/util/Calendar;)J public static fun getFlushShiftMs ()D public static fun getMetricBucketKey (Lio/sentry/metrics/MetricType;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;)Ljava/lang/String; - public static fun getTimeBucketKey (Ljava/util/Calendar;)J + public static fun getTimeBucketKey (J)J public static fun sanitizeKey (Ljava/lang/String;)Ljava/lang/String; public static fun sanitizeValue (Ljava/lang/String;)Ljava/lang/String; public static fun toStatsdType (Lio/sentry/metrics/MetricType;)Ljava/lang/String; @@ -3445,24 +3450,25 @@ public final class io/sentry/metrics/MetricType : java/lang/Enum { public final class io/sentry/metrics/MetricsApi { public fun (Lio/sentry/IMetricAggregator;)V - public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V - public fun gauge (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V - public fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V - public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V - public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V - public fun timing (Ljava/lang/String;Lio/sentry/IMetricAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;Ljava/util/Calendar;I)V + public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V + public fun gauge (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V + public fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V + public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;JI)V + public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;JI)V + public fun timing (Ljava/lang/String;Lio/sentry/IMetricAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;JI)V } public final class io/sentry/metrics/NoopMetricAggregator : io/sentry/IMetricAggregator { public fun ()V public fun close ()V - public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V - public fun gauge (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V + public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V + public fun flush (Z)V + public fun gauge (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public static fun getInstance ()Lio/sentry/metrics/NoopMetricAggregator; - public fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V - public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V - public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;I)V - public fun timing (Ljava/lang/String;Lio/sentry/IMetricAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;Ljava/util/Calendar;I)V + public fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V + public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;JI)V + public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;JI)V + public fun timing (Ljava/lang/String;Lio/sentry/IMetricAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;JI)V } public final class io/sentry/metrics/SentryMetric : io/sentry/JsonSerializable { @@ -3482,7 +3488,7 @@ public final class io/sentry/metrics/SentryMetric$JsonKeys { } public final class io/sentry/metrics/SetMetric : io/sentry/metrics/Metric { - public fun (Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/util/Calendar;)V + public fun (Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun add (D)V public fun getType ()Lio/sentry/metrics/MetricType; public fun getValues ()Ljava/lang/Iterable; diff --git a/sentry/src/main/java/io/sentry/IMetricAggregator.java b/sentry/src/main/java/io/sentry/IMetricAggregator.java index 26b0a4d2eb..9d1667132f 100644 --- a/sentry/src/main/java/io/sentry/IMetricAggregator.java +++ b/sentry/src/main/java/io/sentry/IMetricAggregator.java @@ -1,7 +1,6 @@ package io.sentry; import java.io.Closeable; -import java.util.Calendar; import java.util.Map; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -15,8 +14,8 @@ public interface IMetricAggregator extends Closeable { * @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 timestamp The time when the metric was emitted. Defaults to the time at which the metric - * is emitted, if no value is provided. + * @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( @@ -24,7 +23,7 @@ void increment( final double value, final @Nullable MeasurementUnit unit, final @Nullable Map tags, - final @Nullable Calendar timestamp, + final long timestampMs, final int stackLevel); /** @@ -34,8 +33,8 @@ void increment( * @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 timestamp The time when the metric was emitted. Defaults to the time at which the metric - * is emitted, if no value is provided. + * @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( @@ -43,7 +42,7 @@ void gauge( final double value, final @Nullable MeasurementUnit unit, final @Nullable Map tags, - final @Nullable Calendar timestamp, + final long timestampMs, final int stackLevel); /** @@ -53,8 +52,8 @@ void gauge( * @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 timestamp The time when the metric was emitted. Defaults to the time at which the metric - * is emitted, if no value is provided. + * @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( @@ -62,7 +61,7 @@ void distribution( final double value, final @Nullable MeasurementUnit unit, final @Nullable Map tags, - final @Nullable Calendar timestamp, + final long timestampMs, final int stackLevel); /** @@ -72,8 +71,8 @@ void distribution( * @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 timestamp The time when the metric was emitted. Defaults to the time at which the metric - * is emitted, if no value is provided. + * @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( @@ -81,7 +80,7 @@ void set( final int value, final @Nullable MeasurementUnit unit, final @Nullable Map tags, - final @Nullable Calendar timestamp, + final long timestampMs, final int stackLevel); /** @@ -91,8 +90,8 @@ void set( * @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 timestamp The time when the metric was emitted. Defaults to the time at which the metric - * is emitted, if no value is provided. + * @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( @@ -100,7 +99,7 @@ void set( final @NotNull String value, final @Nullable MeasurementUnit unit, final @Nullable Map tags, - final @Nullable Calendar timestamp, + final long timestampMs, final int stackLevel); /** @@ -110,7 +109,7 @@ void set( * @param callback The code block to measure * @param unit An optional unit, see {@link MeasurementUnit.Duration} * @param tags Optional Tags to associate with the metric - * @param timestamp The time when the metric was emitted + * @param timestampMs The time when the metric was emitted * @param stackLevel Optional number of stacks levels to ignore when determining the code location */ void timing( @@ -118,9 +117,11 @@ void timing( final @NotNull TimingCallback callback, final @NotNull MeasurementUnit.Duration unit, final @Nullable Map tags, - final @Nullable Calendar timestamp, + final long timestampMs, final int stackLevel); + void flush(boolean force); + interface TimingCallback { void run(); } diff --git a/sentry/src/main/java/io/sentry/MetricAggregator.java b/sentry/src/main/java/io/sentry/MetricAggregator.java index a12cea6ac7..8927db08ab 100644 --- a/sentry/src/main/java/io/sentry/MetricAggregator.java +++ b/sentry/src/main/java/io/sentry/MetricAggregator.java @@ -11,7 +11,6 @@ import io.sentry.metrics.SetMetric; import java.io.Closeable; import java.io.IOException; -import java.util.Calendar; import java.util.HashMap; import java.util.Map; import java.util.NavigableMap; @@ -20,12 +19,14 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; @ApiStatus.Internal public final class MetricAggregator implements IMetricAggregator, Runnable, Closeable { private final @NotNull IMetricsHub hub; private final @NotNull ILogger logger; + private @NotNull TimeProvider timeProvider = System::currentTimeMillis; private volatile @NotNull ISentryExecutorService executorService; private volatile boolean isClosed = false; @@ -49,9 +50,9 @@ public void increment( double value, @Nullable MeasurementUnit unit, @Nullable Map tags, - @Nullable Calendar timestamp, + final long timestampMs, int stackLevel) { - add(MetricType.Counter, key, value, unit, tags, timestamp, stackLevel); + add(MetricType.Counter, key, value, unit, tags, timestampMs, stackLevel); } @Override @@ -60,9 +61,9 @@ public void gauge( double value, @Nullable MeasurementUnit unit, @Nullable Map tags, - @Nullable Calendar timestamp, + final long timestampMs, int stackLevel) { - add(MetricType.Gauge, key, value, unit, tags, timestamp, stackLevel); + add(MetricType.Gauge, key, value, unit, tags, timestampMs, stackLevel); } @Override @@ -71,9 +72,9 @@ public void distribution( double value, @Nullable MeasurementUnit unit, @Nullable Map tags, - @Nullable Calendar timestamp, + final long timestampMs, int stackLevel) { - add(MetricType.Distribution, key, value, unit, tags, timestamp, stackLevel); + add(MetricType.Distribution, key, value, unit, tags, timestampMs, stackLevel); } @Override @@ -82,9 +83,9 @@ public void set( int value, @Nullable MeasurementUnit unit, @Nullable Map tags, - @Nullable Calendar timestamp, + final long timestampMs, int stackLevel) { - add(MetricType.Set, key, value, unit, tags, timestamp, stackLevel); + add(MetricType.Set, key, value, unit, tags, timestampMs, stackLevel); } @Override @@ -93,11 +94,11 @@ public void set( @NotNull String value, @Nullable MeasurementUnit unit, @Nullable Map tags, - @Nullable Calendar timestamp, + final long timestampMs, int stackLevel) { // TODO consider using CR32 instead of hashCode // see https://develop.sentry.dev/sdk/metrics/#sets - add(MetricType.Set, key, value.hashCode(), unit, tags, timestamp, stackLevel); + add(MetricType.Set, key, value.hashCode(), unit, tags, timestampMs, stackLevel); } @Override @@ -106,7 +107,7 @@ public void timing( @NotNull TimingCallback callback, @NotNull MeasurementUnit.Duration unit, @Nullable Map tags, - @Nullable Calendar timestamp, + final long timestampMs, int stackLevel) { final long start = System.nanoTime(); try { @@ -114,7 +115,7 @@ public void timing( } finally { final long durationNanos = (System.nanoTime() - start); final double value = MetricHelper.convertNanosTo(unit, durationNanos); - add(MetricType.Distribution, key, value, unit, tags, timestamp, stackLevel + 1); + add(MetricType.Distribution, key, value, unit, tags, timestampMs, stackLevel + 1); } } @@ -125,25 +126,26 @@ private void add( final double value, @Nullable MeasurementUnit unit, final @Nullable Map tags, - @Nullable Calendar timestamp, + @Nullable Long timestampMs, final int stackLevel) { - if (timestamp == null) { - timestamp = Calendar.getInstance(); + + if (timestampMs == null) { + timestampMs = timeProvider.getTimeMillis(); } final @NotNull Metric metric; switch (type) { case Counter: - metric = new CounterMetric(key, value, unit, tags, timestamp); + metric = new CounterMetric(key, value, unit, tags, timestampMs); break; case Gauge: - metric = new GaugeMetric(key, value, unit, tags, timestamp); + metric = new GaugeMetric(key, value, unit, tags, timestampMs); break; case Distribution: - metric = new DistributionMetric(key, value, unit, tags, timestamp); + metric = new DistributionMetric(key, value, unit, tags, timestampMs); break; case Set: - metric = new SetMetric(key, unit, tags, timestamp); + metric = new SetMetric(key, unit, tags, timestampMs); //noinspection unchecked metric.add((int) value); break; @@ -151,7 +153,7 @@ private void add( throw new IllegalArgumentException("Unknown MetricType: " + type.name()); } - final long timeBucketKey = MetricHelper.getTimeBucketKey(timestamp); + final long timeBucketKey = MetricHelper.getTimeBucketKey(timestampMs); final @NotNull Map timeBucket = getOrAddTimeBucket(timeBucketKey); final @NotNull String metricKey = MetricHelper.getMetricBucketKey(type, key, unit, tags); @@ -175,6 +177,7 @@ private void add( } } + @Override public void flush(final boolean force) { final @NotNull Set flushableBuckets = getFlushableBuckets(force); if (flushableBuckets.isEmpty()) { @@ -207,7 +210,9 @@ public Set getFlushableBuckets(final boolean force) { return buckets.keySet(); } else { // get all keys, including the cutoff key - final long cutoffKey = MetricHelper.getTimeBucketKey(MetricHelper.getCutoff()); + final long cutoffTimestampMs = + MetricHelper.getCutoffTimestampMs(timeProvider.getTimeMillis()); + final long cutoffKey = MetricHelper.getTimeBucketKey(cutoffTimestampMs); return buckets.headMap(cutoffKey, true).keySet(); } } @@ -248,4 +253,13 @@ public void run() { executorService.schedule(this, MetricHelper.FLUSHER_SLEEP_TIME_MS); } } + + @TestOnly + void setTimeProvider(final @NotNull TimeProvider timeProvider) { + this.timeProvider = timeProvider; + } + + public interface TimeProvider { + long getTimeMillis(); + } } diff --git a/sentry/src/main/java/io/sentry/metrics/CounterMetric.java b/sentry/src/main/java/io/sentry/metrics/CounterMetric.java index 3bea340a8d..d515024c9f 100644 --- a/sentry/src/main/java/io/sentry/metrics/CounterMetric.java +++ b/sentry/src/main/java/io/sentry/metrics/CounterMetric.java @@ -1,7 +1,6 @@ package io.sentry.metrics; import io.sentry.MeasurementUnit; -import java.util.Calendar; import java.util.Collections; import java.util.Map; import org.jetbrains.annotations.ApiStatus; @@ -18,7 +17,7 @@ public CounterMetric( final double value, final @Nullable MeasurementUnit unit, final @Nullable Map tags, - final @NotNull Calendar timestamp) { + final @NotNull Long timestamp) { super(key, unit, tags, timestamp); this.value = value; } diff --git a/sentry/src/main/java/io/sentry/metrics/DistributionMetric.java b/sentry/src/main/java/io/sentry/metrics/DistributionMetric.java index 286cb51e72..f013018e62 100644 --- a/sentry/src/main/java/io/sentry/metrics/DistributionMetric.java +++ b/sentry/src/main/java/io/sentry/metrics/DistributionMetric.java @@ -2,7 +2,6 @@ import io.sentry.MeasurementUnit; import java.util.ArrayList; -import java.util.Calendar; import java.util.List; import java.util.Map; import org.jetbrains.annotations.ApiStatus; @@ -19,7 +18,7 @@ public DistributionMetric( final double value, final @Nullable MeasurementUnit unit, final @Nullable Map tags, - final @NotNull Calendar timestamp) { + final @NotNull Long timestamp) { super(key, unit, tags, timestamp); this.values = new ArrayList<>(); this.values.add(value); diff --git a/sentry/src/main/java/io/sentry/metrics/GaugeMetric.java b/sentry/src/main/java/io/sentry/metrics/GaugeMetric.java index b20a94494e..675a75cecb 100644 --- a/sentry/src/main/java/io/sentry/metrics/GaugeMetric.java +++ b/sentry/src/main/java/io/sentry/metrics/GaugeMetric.java @@ -2,7 +2,6 @@ import io.sentry.MeasurementUnit; import java.util.Arrays; -import java.util.Calendar; import java.util.Map; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -23,7 +22,7 @@ public GaugeMetric( final double value, final @Nullable MeasurementUnit unit, final @Nullable Map tags, - final @NotNull Calendar timestamp) { + final @NotNull Long timestamp) { super(key, unit, tags, timestamp); this.last = value; diff --git a/sentry/src/main/java/io/sentry/metrics/Metric.java b/sentry/src/main/java/io/sentry/metrics/Metric.java index 2eb9c833af..6a6aa52d83 100644 --- a/sentry/src/main/java/io/sentry/metrics/Metric.java +++ b/sentry/src/main/java/io/sentry/metrics/Metric.java @@ -1,7 +1,6 @@ package io.sentry.metrics; import io.sentry.MeasurementUnit; -import java.util.Calendar; import java.util.Map; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -14,7 +13,7 @@ public abstract class Metric { private final @NotNull String key; private final @Nullable MeasurementUnit unit; private final @Nullable Map tags; - private final @NotNull Calendar timestamp; + private final @NotNull Long timestampMs; /** * Creates a new instance of {@link Metric}. @@ -23,17 +22,17 @@ public abstract class Metric { * @param unit An optional {@link MeasurementUnit} that describes the values being tracked * @param tags An optional set of key/value pairs that can be used to add dimensionality to * metrics - * @param timestamp A time when the metric was emitted. + * @param timestampMs A time when the metric was emitted. */ public Metric( @NotNull String key, @Nullable MeasurementUnit unit, @Nullable Map tags, - @NotNull Calendar timestamp) { + @NotNull Long timestampMs) { this.key = key; this.unit = unit; this.tags = tags; - this.timestamp = timestamp; + this.timestampMs = timestampMs; } /** Adds a value to the metric */ @@ -58,8 +57,9 @@ public Map getTags() { return tags; } - public Calendar getTimeStamp() { - return timestamp; + /** the unix timestamp in milliseconds when the metric was emitted. */ + public Long getTimeStampMs() { + return timestampMs; } public abstract @NotNull Iterable getValues(); diff --git a/sentry/src/main/java/io/sentry/metrics/MetricHelper.java b/sentry/src/main/java/io/sentry/metrics/MetricHelper.java index 4417a00ffe..ac981b01d4 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricHelper.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricHelper.java @@ -22,7 +22,7 @@ public final class MetricHelper { private static final char TAGS_KEY_VALUE_DELIMITER = '='; // Delimiter between key and value private static final char TAGS_ESCAPE_CHAR = '\\'; - private static final double FLUSH_SHIFT_MS = + private static final long FLUSH_SHIFT_MS = (long) (new Random().nextFloat() * (ROLLUP_IN_SECONDS * 1000f)); public static long getDayBucketKey(final @NotNull Calendar timestamp) { @@ -34,8 +34,8 @@ public static long getDayBucketKey(final @NotNull Calendar timestamp) { return utc.getTimeInMillis() / 1000; } - public static long getTimeBucketKey(final @NotNull Calendar timestamp) { - final long seconds = timestamp.getTimeInMillis() / 1000; + public static long getTimeBucketKey(final long timestampMs) { + final long seconds = timestampMs / 1000; return (seconds / ROLLUP_IN_SECONDS) * ROLLUP_IN_SECONDS; } @@ -43,11 +43,8 @@ public static double getFlushShiftMs() { return FLUSH_SHIFT_MS; } - public static Calendar getCutoff() { - final Calendar cutOff = Calendar.getInstance(); - cutOff.add(Calendar.SECOND, -ROLLUP_IN_SECONDS); - cutOff.add(Calendar.MILLISECOND, (int) -FLUSH_SHIFT_MS); - return cutOff; + public static long getCutoffTimestampMs(final long nowMs) { + return nowMs - (ROLLUP_IN_SECONDS * 1000) - FLUSH_SHIFT_MS; } public static @NotNull String sanitizeKey(final @NotNull String input) { diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java index 48f92abd17..6d906634c6 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java @@ -2,7 +2,6 @@ import io.sentry.IMetricAggregator; import io.sentry.MeasurementUnit; -import java.util.Calendar; import java.util.Map; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -23,8 +22,8 @@ public MetricsApi(final @NotNull IMetricAggregator aggregator) { * @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 timestamp The time when the metric was emitted. Defaults to the time at which the metric - * is emitted, if no value is provided. + * @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 */ public void increment( @@ -32,9 +31,9 @@ public void increment( final double value, final @Nullable MeasurementUnit unit, final @Nullable Map tags, - final @Nullable Calendar timestamp, + final long timestampMs, final int stackLevel) { - aggregator.increment(key, value, unit, tags, timestamp, stackLevel); + aggregator.increment(key, value, unit, tags, timestampMs, stackLevel); } /** @@ -44,8 +43,8 @@ public void increment( * @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 timestamp The time when the metric was emitted. Defaults to the time at which the metric - * is emitted, if no value is provided. + * @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 */ public void gauge( @@ -53,9 +52,9 @@ public void gauge( final double value, final @Nullable MeasurementUnit unit, final @Nullable Map tags, - final @Nullable Calendar timestamp, + final long timestampMs, final int stackLevel) { - aggregator.gauge(key, value, unit, tags, timestamp, stackLevel); + aggregator.gauge(key, value, unit, tags, timestampMs, stackLevel); } /** @@ -65,8 +64,8 @@ public void gauge( * @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 timestamp The time when the metric was emitted. Defaults to the time at which the metric - * is emitted, if no value is provided. + * @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 */ public void distribution( @@ -74,9 +73,9 @@ public void distribution( final double value, final @Nullable MeasurementUnit unit, final @Nullable Map tags, - final @Nullable Calendar timestamp, + final long timestampMs, final int stackLevel) { - aggregator.distribution(key, value, unit, tags, timestamp, stackLevel); + aggregator.distribution(key, value, unit, tags, timestampMs, stackLevel); } /** @@ -86,8 +85,8 @@ public void distribution( * @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 timestamp The time when the metric was emitted. Defaults to the time at which the metric - * is emitted, if no value is provided. + * @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 */ public void set( @@ -95,9 +94,9 @@ public void set( final int value, final @Nullable MeasurementUnit unit, final @Nullable Map tags, - final @Nullable Calendar timestamp, + final long timestampMs, final int stackLevel) { - aggregator.set(key, value, unit, tags, timestamp, stackLevel); + aggregator.set(key, value, unit, tags, timestampMs, stackLevel); } /** @@ -107,8 +106,8 @@ public void set( * @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 timestamp The time when the metric was emitted. Defaults to the time at which the metric - * is emitted, if no value is provided. + * @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 */ public void set( @@ -116,9 +115,9 @@ public void set( final @NotNull String value, final @Nullable MeasurementUnit unit, final @Nullable Map tags, - final @Nullable Calendar timestamp, + final long timestampMs, final int stackLevel) { - aggregator.set(key, value, unit, tags, timestamp, stackLevel); + aggregator.set(key, value, unit, tags, timestampMs, stackLevel); } /** @@ -128,7 +127,7 @@ public void set( * @param callback The code block to measure * @param unit An optional unit, see {@link MeasurementUnit.Duration} * @param tags Optional Tags to associate with the metric - * @param timestamp The time when the metric was emitted + * @param timestampMs The time when the metric was emitted * @param stackLevel Optional number of stacks levels to ignore when determining the code location */ public void timing( @@ -136,8 +135,8 @@ public void timing( final @NotNull IMetricAggregator.TimingCallback callback, final @NotNull MeasurementUnit.Duration unit, final @Nullable Map tags, - final @Nullable Calendar timestamp, + final long timestampMs, final int stackLevel) { - aggregator.timing(key, callback, unit, tags, timestamp, stackLevel); + aggregator.timing(key, callback, unit, tags, timestampMs, stackLevel); } } diff --git a/sentry/src/main/java/io/sentry/metrics/NoopMetricAggregator.java b/sentry/src/main/java/io/sentry/metrics/NoopMetricAggregator.java index 99a962bc09..85be81f7c5 100644 --- a/sentry/src/main/java/io/sentry/metrics/NoopMetricAggregator.java +++ b/sentry/src/main/java/io/sentry/metrics/NoopMetricAggregator.java @@ -3,7 +3,6 @@ import io.sentry.IMetricAggregator; import io.sentry.MeasurementUnit; import java.io.IOException; -import java.util.Calendar; import java.util.Map; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -24,7 +23,7 @@ public void increment( double value, @Nullable MeasurementUnit unit, @Nullable Map tags, - @Nullable Calendar timestamp, + long timestampMs, int stackLevel) {} @Override @@ -33,7 +32,7 @@ public void gauge( double value, @Nullable MeasurementUnit unit, @Nullable Map tags, - @Nullable Calendar timestamp, + long timestampMs, int stackLevel) {} @Override @@ -42,7 +41,7 @@ public void distribution( double value, @Nullable MeasurementUnit unit, @Nullable Map tags, - @Nullable Calendar timestamp, + long timestampMs, int stackLevel) {} @Override @@ -51,7 +50,7 @@ public void set( int value, @Nullable MeasurementUnit unit, @Nullable Map tags, - @Nullable Calendar timestamp, + long timestampMs, int stackLevel) {} @Override @@ -60,7 +59,7 @@ public void set( @NotNull String value, @Nullable MeasurementUnit unit, @Nullable Map tags, - @Nullable Calendar timestamp, + long timestampMs, int stackLevel) {} @Override @@ -69,9 +68,14 @@ public void timing( @NotNull TimingCallback callback, MeasurementUnit.@NotNull Duration unit, @Nullable Map tags, - @Nullable Calendar timestamp, + long timestampMs, int stackLevel) {} + @Override + public void flush(boolean force) { + // no-op + } + @Override public void close() throws IOException {} } diff --git a/sentry/src/main/java/io/sentry/metrics/SentryMetric.java b/sentry/src/main/java/io/sentry/metrics/SentryMetric.java index cc94d4c8eb..fd0a2be65f 100644 --- a/sentry/src/main/java/io/sentry/metrics/SentryMetric.java +++ b/sentry/src/main/java/io/sentry/metrics/SentryMetric.java @@ -6,7 +6,6 @@ import io.sentry.ObjectWriter; import io.sentry.protocol.SentryId; import java.io.IOException; -import java.util.Calendar; import java.util.Map; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -33,7 +32,7 @@ public static final class JsonKeys { private final @NotNull String key; private final @Nullable MeasurementUnit unit; private final @Nullable Map tags; - private final @NotNull Calendar timestamp; + private final long timestampMs; public SentryMetric(@NotNull Metric metric) { this.eventId = new SentryId(); @@ -42,7 +41,7 @@ public SentryMetric(@NotNull Metric metric) { this.key = metric.getKey(); this.unit = metric.getUnit(); this.tags = metric.getTags(); - this.timestamp = metric.getTimeStamp(); + this.timestampMs = metric.getTimeStampMs(); this.values = metric.getValues(); } @@ -53,7 +52,7 @@ public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) thr writer.name(JsonKeys.TYPE).value(MetricHelper.toStatsdType(type)); writer.name(JsonKeys.EVENT_ID).value(logger, eventId); writer.name(JsonKeys.NAME).value(key); - writer.name(JsonKeys.TIMESTAMP).value(timestamp.getTimeInMillis() / 1000.0d); + writer.name(JsonKeys.TIMESTAMP).value((double) timestampMs / 1000.0d); if (unit != null) { writer.name(JsonKeys.UNIT).value(unit.apiName()); } diff --git a/sentry/src/main/java/io/sentry/metrics/SetMetric.java b/sentry/src/main/java/io/sentry/metrics/SetMetric.java index 6875b9ba83..a62ff80bee 100644 --- a/sentry/src/main/java/io/sentry/metrics/SetMetric.java +++ b/sentry/src/main/java/io/sentry/metrics/SetMetric.java @@ -1,7 +1,6 @@ package io.sentry.metrics; import io.sentry.MeasurementUnit; -import java.util.Calendar; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -19,7 +18,7 @@ public SetMetric( final @NotNull String key, final @Nullable MeasurementUnit unit, final @Nullable Map tags, - final @NotNull Calendar timestamp) { + final @NotNull Long timestamp) { super(key, unit, tags, timestamp); this.values = new HashSet<>(); } diff --git a/sentry/src/test/java/io/sentry/MetricAggregatorTest.kt b/sentry/src/test/java/io/sentry/MetricAggregatorTest.kt new file mode 100644 index 0000000000..ddcc1b14ff --- /dev/null +++ b/sentry/src/test/java/io/sentry/MetricAggregatorTest.kt @@ -0,0 +1,64 @@ +package io.sentry + +import io.sentry.metrics.IMetricsHub +import io.sentry.metrics.NoopMetricAggregator +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.reset +import org.mockito.kotlin.verify +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test + +class MetricAggregatorTest { + + private val hub = mock() + private val logger = mock() + private var currentTimeMillis: Long = 1000 + private var aggregator: IMetricAggregator = NoopMetricAggregator() + + @BeforeTest + fun setup() { + aggregator = MetricAggregator(hub, logger).also { + it.setTimeProvider { + currentTimeMillis + } + } + reset(hub) + } + + @AfterTest + fun tearDown() { + aggregator.close() + aggregator = NoopMetricAggregator() + } + + @Test + fun `flush is a no-op when there's nothing to flush`() { + // when no metrics are collected + + // then flush does nothing + aggregator.flush(false) + + verify(hub, never()).captureMetrics(any()) + } + + @Test + fun `flush performs a flush`() { + // when a metric is emitted + currentTimeMillis = 2000 + aggregator.increment("key", 1.0, null, null, 2001, 1) + + // then flush does nothing because there's no data inside the flush interval + aggregator.flush(false) + verify(hub, never()).captureMetrics(any()) + + // as times moves on + currentTimeMillis = 20_000 + + // the metric should be flushed + aggregator.flush(false) + verify(hub).captureMetrics(any()) + } +} diff --git a/sentry/src/test/java/io/sentry/metrics/CounterMetricTest.kt b/sentry/src/test/java/io/sentry/metrics/CounterMetricTest.kt index d94a027a21..8ca09ae4eb 100644 --- a/sentry/src/test/java/io/sentry/metrics/CounterMetricTest.kt +++ b/sentry/src/test/java/io/sentry/metrics/CounterMetricTest.kt @@ -1,6 +1,5 @@ package io.sentry.metrics -import java.util.Calendar import kotlin.test.Test import kotlin.test.assertEquals @@ -16,7 +15,7 @@ class CounterMetricTest { "tag1" to "value1", "tag2" to "value2" ), - Calendar.getInstance() + System.currentTimeMillis() ) assertEquals(1.0, metric.value) @@ -39,7 +38,7 @@ class CounterMetricTest { "tag1" to "value1", "tag2" to "value2" ), - Calendar.getInstance() + System.currentTimeMillis() ) assertEquals(MetricType.Counter, metric.type) } @@ -54,7 +53,7 @@ class CounterMetricTest { "tag1" to "value1", "tag2" to "value2" ), - Calendar.getInstance() + System.currentTimeMillis() ) assertEquals(1, metric.weight) } @@ -69,7 +68,7 @@ class CounterMetricTest { "tag1" to "value1", "tag2" to "value2" ), - Calendar.getInstance() + System.currentTimeMillis() ) val values0 = metric.values.toList() diff --git a/sentry/src/test/java/io/sentry/metrics/DistributionMetricTest.kt b/sentry/src/test/java/io/sentry/metrics/DistributionMetricTest.kt index 70677f0398..98c08cf3cc 100644 --- a/sentry/src/test/java/io/sentry/metrics/DistributionMetricTest.kt +++ b/sentry/src/test/java/io/sentry/metrics/DistributionMetricTest.kt @@ -1,6 +1,5 @@ package io.sentry.metrics -import java.util.Calendar import kotlin.test.Test import kotlin.test.assertEquals @@ -13,7 +12,7 @@ class DistributionMetricTest { 1.0, null, null, - Calendar.getInstance() + System.currentTimeMillis() ) assertEquals(listOf(1.0), metric.values.toList()) @@ -29,7 +28,7 @@ class DistributionMetricTest { 1.0, null, null, - Calendar.getInstance() + System.currentTimeMillis() ) assertEquals(MetricType.Distribution, metric.type) } @@ -41,7 +40,7 @@ class DistributionMetricTest { 1.0, null, null, - Calendar.getInstance() + System.currentTimeMillis() ) assertEquals(1, metric.weight) @@ -56,7 +55,7 @@ class DistributionMetricTest { 1.0, null, null, - Calendar.getInstance() + System.currentTimeMillis() ) metric.add(2.0) diff --git a/sentry/src/test/java/io/sentry/metrics/GaugeMetricTest.kt b/sentry/src/test/java/io/sentry/metrics/GaugeMetricTest.kt index a83759fe7f..f3e83cc734 100644 --- a/sentry/src/test/java/io/sentry/metrics/GaugeMetricTest.kt +++ b/sentry/src/test/java/io/sentry/metrics/GaugeMetricTest.kt @@ -1,6 +1,5 @@ package io.sentry.metrics -import java.util.Calendar import kotlin.test.Test import kotlin.test.assertEquals @@ -13,7 +12,7 @@ class GaugeMetricTest { 1.0, null, null, - Calendar.getInstance() + System.currentTimeMillis() ) assertEquals( listOf( @@ -50,7 +49,7 @@ class GaugeMetricTest { 1.0, null, null, - Calendar.getInstance() + System.currentTimeMillis() ) assertEquals(MetricType.Gauge, metric.type) } @@ -62,7 +61,7 @@ class GaugeMetricTest { 1.0, null, null, - Calendar.getInstance() + System.currentTimeMillis() ) assertEquals(5, metric.weight) diff --git a/sentry/src/test/java/io/sentry/metrics/MetricHelperTest.kt b/sentry/src/test/java/io/sentry/metrics/MetricHelperTest.kt index e42e8c29f2..e2b9f3f6dc 100644 --- a/sentry/src/test/java/io/sentry/metrics/MetricHelperTest.kt +++ b/sentry/src/test/java/io/sentry/metrics/MetricHelperTest.kt @@ -1,6 +1,5 @@ package io.sentry.metrics -import java.util.Calendar import kotlin.test.Test import kotlin.test.assertEquals @@ -24,47 +23,27 @@ class MetricHelperTest { fun getTimeBucketKey() { assertEquals( 10, - MetricHelper.getTimeBucketKey( - Calendar.getInstance().apply { - timeInMillis = 10_000 - } - ) + MetricHelper.getTimeBucketKey(10_000) ) assertEquals( 10, - MetricHelper.getTimeBucketKey( - Calendar.getInstance().apply { - timeInMillis = 10_001 - } - ) + MetricHelper.getTimeBucketKey(10_001) ) assertEquals( 20, - MetricHelper.getTimeBucketKey( - Calendar.getInstance().apply { - timeInMillis = 20_000 - } - ) + MetricHelper.getTimeBucketKey(20_000) ) assertEquals( 20, - MetricHelper.getTimeBucketKey( - Calendar.getInstance().apply { - timeInMillis = 29_999 - } - ) + MetricHelper.getTimeBucketKey(29_999) ) assertEquals( 30, - MetricHelper.getTimeBucketKey( - Calendar.getInstance().apply { - timeInMillis = 30_000 - } - ) + MetricHelper.getTimeBucketKey(30_000) ) } } diff --git a/sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt b/sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt index 3f339b39dd..a12db49976 100644 --- a/sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt +++ b/sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt @@ -1,6 +1,5 @@ package io.sentry.metrics -import java.util.Calendar import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -13,7 +12,7 @@ class SetMetricTest { "test", null, null, - Calendar.getInstance() + System.currentTimeMillis() ) assertTrue(metric.values.toList().isEmpty()) @@ -35,7 +34,7 @@ class SetMetricTest { "test", null, null, - Calendar.getInstance() + System.currentTimeMillis() ) assertEquals(MetricType.Set, metric.type) } @@ -46,7 +45,7 @@ class SetMetricTest { "test", null, null, - Calendar.getInstance() + System.currentTimeMillis() ) assertEquals(0, metric.weight) From 1809236d3266b4a3a980f85fb7fb6ce5fea13bbb Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Thu, 15 Feb 2024 12:16:20 +0100 Subject: [PATCH 04/26] Rename to metrics, have Sentry.metrics() as entry point --- .../sentry/samples/android/MyApplication.java | 2 +- sentry/api/sentry.api | 73 ++++++++++--------- sentry/src/main/java/io/sentry/Hub.java | 6 +- .../src/main/java/io/sentry/HubAdapter.java | 4 +- sentry/src/main/java/io/sentry/IHub.java | 2 +- ...ggregator.java => IMetricsAggregator.java} | 2 +- ...Aggregator.java => MetricsAggregator.java} | 48 ++++++++---- sentry/src/main/java/io/sentry/NoOpHub.java | 7 +- sentry/src/main/java/io/sentry/Sentry.java | 4 +- .../io/sentry/metrics/EncodedMetrics.java | 2 +- .../metrics/MetricResourceIdentifier.java | 2 +- .../java/io/sentry/metrics/MetricsApi.java | 44 +++++++---- .../{MetricHelper.java => MetricsHelper.java} | 44 ++++++++--- ...egator.java => NoopMetricsAggregator.java} | 8 +- .../java/io/sentry/metrics/SentryMetric.java | 2 +- ...egatorTest.kt => MetricsAggregatorTest.kt} | 23 +++--- .../io/sentry/metrics/MetricHelperTest.kt | 49 ------------- .../io/sentry/metrics/MetricsHelperTest.kt | 62 ++++++++++++++++ .../java/io/sentry/metrics/SetMetricTest.kt | 8 +- 19 files changed, 231 insertions(+), 161 deletions(-) rename sentry/src/main/java/io/sentry/{IMetricAggregator.java => IMetricsAggregator.java} (98%) rename sentry/src/main/java/io/sentry/{MetricAggregator.java => MetricsAggregator.java} (82%) rename sentry/src/main/java/io/sentry/metrics/{MetricHelper.java => MetricsHelper.java} (79%) rename sentry/src/main/java/io/sentry/metrics/{NoopMetricAggregator.java => NoopMetricsAggregator.java} (87%) rename sentry/src/test/java/io/sentry/{MetricAggregatorTest.kt => MetricsAggregatorTest.kt} (70%) delete mode 100644 sentry/src/test/java/io/sentry/metrics/MetricHelperTest.kt create mode 100644 sentry/src/test/java/io/sentry/metrics/MetricsHelperTest.kt diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java index 11b47107a9..c52f267286 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java @@ -26,7 +26,7 @@ public void onCreate() { // */ // }); - Sentry.getMetricsApi().increment("app.start.cold", 1, null, null, null, 0); + Sentry.metrics().increment("app.start.cold", 1, null, null, 0, 0); } private void strictMode() { diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 05ef4d8e9f..1cee511dc1 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -434,7 +434,6 @@ public final class io/sentry/Hub : io/sentry/IHub, io/sentry/metrics/IMetricsHub public fun flush (J)V public fun getBaggage ()Lio/sentry/BaggageHeader; public fun getLastEventId ()Lio/sentry/protocol/SentryId; - public fun getMetricsApi ()Lio/sentry/metrics/MetricsApi; public fun getOptions ()Lio/sentry/SentryOptions; public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun getSpan ()Lio/sentry/ISpan; @@ -443,6 +442,7 @@ public final class io/sentry/Hub : io/sentry/IHub, io/sentry/metrics/IMetricsHub public fun isCrashedLastRun ()Ljava/lang/Boolean; public fun isEnabled ()Z public fun isHealthy ()Z + public fun metrics ()Lio/sentry/metrics/MetricsApi; public fun popScope ()V public fun pushScope ()V public fun removeExtra (Ljava/lang/String;)V @@ -486,7 +486,6 @@ public final class io/sentry/HubAdapter : io/sentry/IHub { public fun getBaggage ()Lio/sentry/BaggageHeader; public static fun getInstance ()Lio/sentry/HubAdapter; public fun getLastEventId ()Lio/sentry/protocol/SentryId; - public fun getMetricsApi ()Lio/sentry/metrics/MetricsApi; public fun getOptions ()Lio/sentry/SentryOptions; public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun getSpan ()Lio/sentry/ISpan; @@ -495,6 +494,7 @@ public final class io/sentry/HubAdapter : io/sentry/IHub { public fun isCrashedLastRun ()Ljava/lang/Boolean; public fun isEnabled ()Z public fun isHealthy ()Z + public fun metrics ()Lio/sentry/metrics/MetricsApi; public fun popScope ()V public fun pushScope ()V public fun removeExtra (Ljava/lang/String;)V @@ -576,7 +576,6 @@ public abstract interface class io/sentry/IHub { public abstract fun flush (J)V public abstract fun getBaggage ()Lio/sentry/BaggageHeader; public abstract fun getLastEventId ()Lio/sentry/protocol/SentryId; - public abstract fun getMetricsApi ()Lio/sentry/metrics/MetricsApi; public abstract fun getOptions ()Lio/sentry/SentryOptions; public abstract fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public abstract fun getSpan ()Lio/sentry/ISpan; @@ -585,6 +584,7 @@ public abstract interface class io/sentry/IHub { public abstract fun isCrashedLastRun ()Ljava/lang/Boolean; public abstract fun isEnabled ()Z public abstract fun isHealthy ()Z + public abstract fun metrics ()Lio/sentry/metrics/MetricsApi; public abstract fun popScope ()V public abstract fun pushScope ()V public abstract fun removeExtra (Ljava/lang/String;)V @@ -618,17 +618,17 @@ public abstract interface class io/sentry/IMemoryCollector { public abstract fun collect ()Lio/sentry/MemoryCollectionData; } -public abstract interface class io/sentry/IMetricAggregator : java/io/Closeable { +public abstract interface class io/sentry/IMetricsAggregator : java/io/Closeable { public abstract fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public abstract fun flush (Z)V public abstract fun gauge (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public abstract fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public abstract fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public abstract fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;JI)V - public abstract fun timing (Ljava/lang/String;Lio/sentry/IMetricAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;JI)V + public abstract fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;JI)V } -public abstract interface class io/sentry/IMetricAggregator$TimingCallback { +public abstract interface class io/sentry/IMetricsAggregator$TimingCallback { public abstract fun run ()V } @@ -1019,7 +1019,7 @@ public final class io/sentry/MemoryCollectionData { public fun getUsedNativeMemory ()J } -public final class io/sentry/MetricAggregator : io/sentry/IMetricAggregator, java/io/Closeable, java/lang/Runnable { +public final class io/sentry/MetricsAggregator : io/sentry/IMetricsAggregator, java/io/Closeable, java/lang/Runnable { public fun (Lio/sentry/metrics/IMetricsHub;Lio/sentry/ILogger;)V public fun close ()V public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V @@ -1030,10 +1030,10 @@ public final class io/sentry/MetricAggregator : io/sentry/IMetricAggregator, jav public fun run ()V public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;JI)V - public fun timing (Ljava/lang/String;Lio/sentry/IMetricAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;JI)V + public fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;JI)V } -public abstract interface class io/sentry/MetricAggregator$TimeProvider { +public abstract interface class io/sentry/MetricsAggregator$TimeProvider { public abstract fun getTimeMillis ()J } @@ -1174,7 +1174,6 @@ public final class io/sentry/NoOpHub : io/sentry/IHub { public fun getBaggage ()Lio/sentry/BaggageHeader; public static fun getInstance ()Lio/sentry/NoOpHub; public fun getLastEventId ()Lio/sentry/protocol/SentryId; - public fun getMetricsApi ()Lio/sentry/metrics/MetricsApi; public fun getOptions ()Lio/sentry/SentryOptions; public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun getSpan ()Lio/sentry/ISpan; @@ -1183,6 +1182,7 @@ public final class io/sentry/NoOpHub : io/sentry/IHub { public fun isCrashedLastRun ()Ljava/lang/Boolean; public fun isEnabled ()Z public fun isHealthy ()Z + public fun metrics ()Lio/sentry/metrics/MetricsApi; public fun popScope ()V public fun pushScope ()V public fun removeExtra (Ljava/lang/String;)V @@ -1723,7 +1723,6 @@ public final class io/sentry/Sentry { public static fun getBaggage ()Lio/sentry/BaggageHeader; public static fun getCurrentHub ()Lio/sentry/IHub; public static fun getLastEventId ()Lio/sentry/protocol/SentryId; - public static fun getMetricsApi ()Lio/sentry/metrics/MetricsApi; public static fun getSpan ()Lio/sentry/ISpan; public static fun getTraceparent ()Lio/sentry/SentryTraceHeader; public static fun init ()V @@ -1736,6 +1735,7 @@ public final class io/sentry/Sentry { public static fun isCrashedLastRun ()Ljava/lang/Boolean; public static fun isEnabled ()Z public static fun isHealthy ()Z + public static fun metrics ()Lio/sentry/metrics/MetricsApi; public static fun popScope ()V public static fun pushScope ()V public static fun removeExtra (Ljava/lang/String;)V @@ -3414,21 +3414,6 @@ public abstract class io/sentry/metrics/Metric { public abstract fun getWeight ()I } -public final class io/sentry/metrics/MetricHelper { - public static final field FLUSHER_SLEEP_TIME_MS I - public fun ()V - public static fun convertNanosTo (Lio/sentry/MeasurementUnit$Duration;J)D - public static fun encodeMetrics (JLjava/util/Collection;Ljava/lang/StringBuilder;)V - public static fun getCutoffTimestampMs (J)J - public static fun getDayBucketKey (Ljava/util/Calendar;)J - public static fun getFlushShiftMs ()D - public static fun getMetricBucketKey (Lio/sentry/metrics/MetricType;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;)Ljava/lang/String; - public static fun getTimeBucketKey (J)J - public static fun sanitizeKey (Ljava/lang/String;)Ljava/lang/String; - public static fun sanitizeValue (Ljava/lang/String;)Ljava/lang/String; - public static fun toStatsdType (Lio/sentry/metrics/MetricType;)Ljava/lang/String; -} - public final class io/sentry/metrics/MetricResourceIdentifier { public fun (Lio/sentry/metrics/MetricType;Ljava/lang/String;Lio/sentry/MeasurementUnit;)V public fun equals (Ljava/lang/Object;)Z @@ -3449,26 +3434,42 @@ public final class io/sentry/metrics/MetricType : java/lang/Enum { } public final class io/sentry/metrics/MetricsApi { - public fun (Lio/sentry/IMetricAggregator;)V - public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V - public fun gauge (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V - public fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V - public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;JI)V - public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;JI)V - public fun timing (Ljava/lang/String;Lio/sentry/IMetricAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;JI)V + public fun (Lio/sentry/IMetricsAggregator;)V + public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;I)V + public fun gauge (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;I)V + public fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;I)V + public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;I)V + public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;I)V + public fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;Ljava/lang/Long;I)V +} + +public final class io/sentry/metrics/MetricsHelper { + public static final field FLUSHER_SLEEP_TIME_MS I + public fun ()V + public static fun convertNanosTo (Lio/sentry/MeasurementUnit$Duration;J)D + public static fun encodeMetrics (JLjava/util/Collection;Ljava/lang/StringBuilder;)V + public static fun getCutoffTimestampMs (J)J + public static fun getDayBucketKey (Ljava/util/Calendar;)J + public static fun getMetricBucketKey (Lio/sentry/metrics/MetricType;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;)Ljava/lang/String; + public static fun getTimeBucketKey (J)J + public static fun sanitizeKey (Ljava/lang/String;)Ljava/lang/String; + public static fun sanitizeUnit (Ljava/lang/String;)Ljava/lang/String; + public static fun sanitizeValue (Ljava/lang/String;)Ljava/lang/String; + public static fun setFlushShiftMs (J)V + public static fun toStatsdType (Lio/sentry/metrics/MetricType;)Ljava/lang/String; } -public final class io/sentry/metrics/NoopMetricAggregator : io/sentry/IMetricAggregator { +public final class io/sentry/metrics/NoopMetricsAggregator : io/sentry/IMetricsAggregator { public fun ()V public fun close ()V public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public fun flush (Z)V public fun gauge (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V - public static fun getInstance ()Lio/sentry/metrics/NoopMetricAggregator; + public static fun getInstance ()Lio/sentry/metrics/NoopMetricsAggregator; public fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;JI)V - public fun timing (Ljava/lang/String;Lio/sentry/IMetricAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;JI)V + public fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;JI)V } public final class io/sentry/metrics/SentryMetric : io/sentry/JsonSerializable { diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index 28a759ab76..613be675d8 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -37,7 +37,7 @@ public final class Hub implements IHub, IMetricsHub { private final @NotNull Map, String>> throwableToSpan = Collections.synchronizedMap(new WeakHashMap<>()); private final @NotNull TransactionPerformanceCollector transactionPerformanceCollector; - private final @NotNull IMetricAggregator metricAggregator; + private final @NotNull IMetricsAggregator metricAggregator; private final @NotNull MetricsApi metricsApi; public Hub(final @NotNull SentryOptions options) { @@ -58,7 +58,7 @@ private Hub(final @NotNull SentryOptions options, final @NotNull Stack stack) { // Make sure Hub ready to be used then. this.isEnabled = true; - this.metricAggregator = new MetricAggregator(this, options.getLogger()); + this.metricAggregator = new MetricsAggregator(this, options.getLogger()); this.metricsApi = new MetricsApi(metricAggregator); } @@ -968,7 +968,7 @@ private IScope buildLocalScope( } @Override - public @NotNull MetricsApi getMetricsApi() { + public @NotNull MetricsApi metrics() { return metricsApi; } } diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index d4a6cbd8cf..32b8a61fdb 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -270,7 +270,7 @@ public void reportFullyDisplayed() { } @Override - public @NotNull MetricsApi getMetricsApi() { - return Sentry.getCurrentHub().getMetricsApi(); + public @NotNull MetricsApi metrics() { + return Sentry.getCurrentHub().metrics(); } } diff --git a/sentry/src/main/java/io/sentry/IHub.java b/sentry/src/main/java/io/sentry/IHub.java index 61d671fbf5..62ff2a3eea 100644 --- a/sentry/src/main/java/io/sentry/IHub.java +++ b/sentry/src/main/java/io/sentry/IHub.java @@ -579,5 +579,5 @@ TransactionContext continueTrace( @ApiStatus.Experimental @NotNull - MetricsApi getMetricsApi(); + MetricsApi metrics(); } diff --git a/sentry/src/main/java/io/sentry/IMetricAggregator.java b/sentry/src/main/java/io/sentry/IMetricsAggregator.java similarity index 98% rename from sentry/src/main/java/io/sentry/IMetricAggregator.java rename to sentry/src/main/java/io/sentry/IMetricsAggregator.java index 9d1667132f..cfb014fe08 100644 --- a/sentry/src/main/java/io/sentry/IMetricAggregator.java +++ b/sentry/src/main/java/io/sentry/IMetricsAggregator.java @@ -5,7 +5,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public interface IMetricAggregator extends Closeable { +public interface IMetricsAggregator extends Closeable { /** * Emits a Counter metric diff --git a/sentry/src/main/java/io/sentry/MetricAggregator.java b/sentry/src/main/java/io/sentry/MetricsAggregator.java similarity index 82% rename from sentry/src/main/java/io/sentry/MetricAggregator.java rename to sentry/src/main/java/io/sentry/MetricsAggregator.java index 8927db08ab..44904b0807 100644 --- a/sentry/src/main/java/io/sentry/MetricAggregator.java +++ b/sentry/src/main/java/io/sentry/MetricsAggregator.java @@ -6,23 +6,37 @@ import io.sentry.metrics.GaugeMetric; import io.sentry.metrics.IMetricsHub; import io.sentry.metrics.Metric; -import io.sentry.metrics.MetricHelper; import io.sentry.metrics.MetricType; +import io.sentry.metrics.MetricsHelper; import io.sentry.metrics.SetMetric; import java.io.Closeable; import java.io.IOException; +import java.nio.charset.Charset; import java.util.HashMap; import java.util.Map; import java.util.NavigableMap; import java.util.Set; import java.util.concurrent.ConcurrentSkipListMap; +import java.util.zip.CRC32; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; @ApiStatus.Internal -public final class MetricAggregator implements IMetricAggregator, Runnable, Closeable { +public final class MetricsAggregator implements IMetricsAggregator, Runnable, Closeable { + + @SuppressWarnings({"CharsetObjectCanBeUsed"}) + private static final Charset UTF8 = Charset.forName("UTF-8"); + + @SuppressWarnings("AnonymousHasLambdaAlternative") + private static final ThreadLocal crc32 = + new ThreadLocal() { + @Override + protected CRC32 initialValue() { + return new CRC32(); + } + }; private final @NotNull IMetricsHub hub; private final @NotNull ILogger logger; @@ -38,7 +52,7 @@ public final class MetricAggregator implements IMetricAggregator, Runnable, Clos // each of which has a key that uniquely identifies it within the time period private final NavigableMap> buckets = new ConcurrentSkipListMap<>(); - public MetricAggregator(final @NotNull IMetricsHub hub, final @NotNull ILogger logger) { + public MetricsAggregator(final @NotNull IMetricsHub hub, final @NotNull ILogger logger) { this.hub = hub; this.logger = logger; this.executorService = NoOpSentryExecutorService.getInstance(); @@ -96,9 +110,15 @@ public void set( @Nullable Map tags, final long timestampMs, int stackLevel) { - // TODO consider using CR32 instead of hashCode - // see https://develop.sentry.dev/sdk/metrics/#sets - add(MetricType.Set, key, value.hashCode(), unit, tags, timestampMs, stackLevel); + + final byte[] bytes = value.getBytes(UTF8); + + final CRC32 crc = crc32.get(); + crc.reset(); + crc.update(bytes, 0, bytes.length); + final int intValue = (int) crc.getValue(); + + add(MetricType.Set, key, intValue, unit, tags, timestampMs, stackLevel); } @Override @@ -114,7 +134,7 @@ public void timing( callback.run(); } finally { final long durationNanos = (System.nanoTime() - start); - final double value = MetricHelper.convertNanosTo(unit, durationNanos); + final double value = MetricsHelper.convertNanosTo(unit, durationNanos); add(MetricType.Distribution, key, value, unit, tags, timestampMs, stackLevel + 1); } } @@ -153,10 +173,10 @@ private void add( throw new IllegalArgumentException("Unknown MetricType: " + type.name()); } - final long timeBucketKey = MetricHelper.getTimeBucketKey(timestampMs); + final long timeBucketKey = MetricsHelper.getTimeBucketKey(timestampMs); final @NotNull Map timeBucket = getOrAddTimeBucket(timeBucketKey); - final @NotNull String metricKey = MetricHelper.getMetricBucketKey(type, key, unit, tags); + final @NotNull String metricKey = MetricsHelper.getMetricBucketKey(type, key, unit, tags); synchronized (timeBucket) { @Nullable Metric existingMetric = timeBucket.get(metricKey); if (existingMetric != null) { @@ -171,7 +191,7 @@ private void add( synchronized (this) { if (!isClosed && executorService instanceof NoOpSentryExecutorService) { executorService = new SentryExecutorService(); - executorService.schedule(this, MetricHelper.FLUSHER_SLEEP_TIME_MS); + executorService.schedule(this, MetricsHelper.FLUSHER_SLEEP_TIME_MS); } } } @@ -190,7 +210,7 @@ public void flush(final boolean force) { for (long bucketKey : flushableBuckets) { final @Nullable Map metrics = buckets.remove(bucketKey); if (metrics != null) { - MetricHelper.encodeMetrics(bucketKey, metrics.values(), writer); + MetricsHelper.encodeMetrics(bucketKey, metrics.values(), writer); } } @@ -211,8 +231,8 @@ public Set getFlushableBuckets(final boolean force) { } else { // get all keys, including the cutoff key final long cutoffTimestampMs = - MetricHelper.getCutoffTimestampMs(timeProvider.getTimeMillis()); - final long cutoffKey = MetricHelper.getTimeBucketKey(cutoffTimestampMs); + MetricsHelper.getCutoffTimestampMs(timeProvider.getTimeMillis()); + final long cutoffKey = MetricsHelper.getTimeBucketKey(cutoffTimestampMs); return buckets.headMap(cutoffKey, true).keySet(); } } @@ -250,7 +270,7 @@ public void run() { flush(false); if (!isClosed) { - executorService.schedule(this, MetricHelper.FLUSHER_SLEEP_TIME_MS); + executorService.schedule(this, MetricsHelper.FLUSHER_SLEEP_TIME_MS); } } diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index cc2d9d808b..29ea7d49f1 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -1,7 +1,7 @@ package io.sentry; import io.sentry.metrics.MetricsApi; -import io.sentry.metrics.NoopMetricAggregator; +import io.sentry.metrics.NoopMetricsAggregator; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; @@ -16,7 +16,8 @@ public final class NoOpHub implements IHub { private static final NoOpHub instance = new NoOpHub(); private final @NotNull SentryOptions emptyOptions = SentryOptions.empty(); - private final @NotNull MetricsApi metricsApi = new MetricsApi(NoopMetricAggregator.getInstance()); + private final @NotNull MetricsApi metricsApi = + new MetricsApi(NoopMetricsAggregator.getInstance()); private NoOpHub() {} @@ -227,7 +228,7 @@ public void reportFullyDisplayed() {} } @Override - public @NotNull MetricsApi getMetricsApi() { + public @NotNull MetricsApi metrics() { return metricsApi; } } diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index b8d33234e0..218464630d 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -976,8 +976,8 @@ public static void reportFullDisplayed() { /** the metrics API for the current hub */ @NotNull @ApiStatus.Experimental - public static MetricsApi getMetricsApi() { - return getCurrentHub().getMetricsApi(); + public static MetricsApi metrics() { + return getCurrentHub().metrics(); } /** diff --git a/sentry/src/main/java/io/sentry/metrics/EncodedMetrics.java b/sentry/src/main/java/io/sentry/metrics/EncodedMetrics.java index 4322c4b317..44fd681a25 100644 --- a/sentry/src/main/java/io/sentry/metrics/EncodedMetrics.java +++ b/sentry/src/main/java/io/sentry/metrics/EncodedMetrics.java @@ -6,7 +6,7 @@ @ApiStatus.Internal public final class EncodedMetrics { - @SuppressWarnings("unused") + @SuppressWarnings({"CharsetObjectCanBeUsed"}) private static final Charset UTF8 = Charset.forName("UTF-8"); private final @NotNull String statsd; diff --git a/sentry/src/main/java/io/sentry/metrics/MetricResourceIdentifier.java b/sentry/src/main/java/io/sentry/metrics/MetricResourceIdentifier.java index 097f378720..7aebd4f010 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricResourceIdentifier.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricResourceIdentifier.java @@ -44,7 +44,7 @@ public MeasurementUnit getUnit() { @Override public String toString() { return String.format( - "%s:%s@%s", MetricHelper.toStatsdType(metricType), MetricHelper.sanitizeKey(key), unit); + "%s:%s@%s", MetricsHelper.toStatsdType(metricType), MetricsHelper.sanitizeKey(key), unit); } @Override diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java index 6d906634c6..4467c02e20 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java @@ -1,6 +1,6 @@ package io.sentry.metrics; -import io.sentry.IMetricAggregator; +import io.sentry.IMetricsAggregator; import io.sentry.MeasurementUnit; import java.util.Map; import org.jetbrains.annotations.NotNull; @@ -9,9 +9,9 @@ // TODO: add tons of method overloads to make it delightful to use public final class MetricsApi { - private final @NotNull IMetricAggregator aggregator; + private final @NotNull IMetricsAggregator aggregator; - public MetricsApi(final @NotNull IMetricAggregator aggregator) { + public MetricsApi(final @NotNull IMetricsAggregator aggregator) { this.aggregator = aggregator; } @@ -31,9 +31,11 @@ public void increment( final double value, final @Nullable MeasurementUnit unit, final @Nullable Map tags, - final long timestampMs, + final @Nullable Long timestampMs, final int stackLevel) { - aggregator.increment(key, value, unit, tags, timestampMs, stackLevel); + + final long timestamp = timestampMs != null ? timestampMs : System.currentTimeMillis(); + aggregator.increment(key, value, unit, tags, timestamp, stackLevel); } /** @@ -52,9 +54,11 @@ public void gauge( final double value, final @Nullable MeasurementUnit unit, final @Nullable Map tags, - final long timestampMs, + final @Nullable Long timestampMs, final int stackLevel) { - aggregator.gauge(key, value, unit, tags, timestampMs, stackLevel); + + final long timestamp = timestampMs != null ? timestampMs : System.currentTimeMillis(); + aggregator.gauge(key, value, unit, tags, timestamp, stackLevel); } /** @@ -73,9 +77,11 @@ public void distribution( final double value, final @Nullable MeasurementUnit unit, final @Nullable Map tags, - final long timestampMs, + final @Nullable Long timestampMs, final int stackLevel) { - aggregator.distribution(key, value, unit, tags, timestampMs, stackLevel); + + final long timestamp = timestampMs != null ? timestampMs : System.currentTimeMillis(); + aggregator.distribution(key, value, unit, tags, timestamp, stackLevel); } /** @@ -94,9 +100,11 @@ public void set( final int value, final @Nullable MeasurementUnit unit, final @Nullable Map tags, - final long timestampMs, + final @Nullable Long timestampMs, final int stackLevel) { - aggregator.set(key, value, unit, tags, timestampMs, stackLevel); + + final long timestamp = timestampMs != null ? timestampMs : System.currentTimeMillis(); + aggregator.set(key, value, unit, tags, timestamp, stackLevel); } /** @@ -115,9 +123,11 @@ public void set( final @NotNull String value, final @Nullable MeasurementUnit unit, final @Nullable Map tags, - final long timestampMs, + final @Nullable Long timestampMs, final int stackLevel) { - aggregator.set(key, value, unit, tags, timestampMs, stackLevel); + + final long timestamp = timestampMs != null ? timestampMs : System.currentTimeMillis(); + aggregator.set(key, value, unit, tags, timestamp, stackLevel); } /** @@ -132,11 +142,13 @@ public void set( */ public void timing( final @NotNull String key, - final @NotNull IMetricAggregator.TimingCallback callback, + final @NotNull IMetricsAggregator.TimingCallback callback, final @NotNull MeasurementUnit.Duration unit, final @Nullable Map tags, - final long timestampMs, + final @Nullable Long timestampMs, final int stackLevel) { - aggregator.timing(key, callback, unit, tags, timestampMs, stackLevel); + + final long timestamp = timestampMs != null ? timestampMs : System.currentTimeMillis(); + aggregator.timing(key, callback, unit, tags, timestamp, stackLevel); } } diff --git a/sentry/src/main/java/io/sentry/metrics/MetricHelper.java b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java similarity index 79% rename from sentry/src/main/java/io/sentry/metrics/MetricHelper.java rename to sentry/src/main/java/io/sentry/metrics/MetricsHelper.java index ac981b01d4..fda3fb7480 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricHelper.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java @@ -6,23 +6,31 @@ import java.util.Map; import java.util.Random; import java.util.TimeZone; +import java.util.regex.Pattern; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; @ApiStatus.Internal -public final class MetricHelper { +public final class MetricsHelper { public static final int FLUSHER_SLEEP_TIME_MS = 5000; private static final int ROLLUP_IN_SECONDS = 10; - private static final String INVALID_KEY_CHARACTERS_PATTERN = "[^a-zA-Z0-9_/.-]+"; - private static final String INVALID_VALUE_CHARACTERS_PATTERN = "[^\\w\\d_:/@\\.\\{\\}\\[\\]$-]+"; + private static final Pattern INVALID_KEY_CHARACTERS_PATTERN = + Pattern.compile("[^a-zA-Z0-9_/.-]+"); + private static final Pattern INVALID_VALUE_CHARACTERS_PATTERN = + Pattern.compile("[^\\w\\d_:/@\\.\\{\\}\\[\\]$-]+"); + // See + // https://docs.sysdig.com/en/docs/sysdig-monitor/integrations/working-with-integrations/custom-integrations/integrate-statsd-metrics/#characters-allowed-for-statsd-metric-names + private static final Pattern INVALID_METRIC_UNIT_CHARACTERS_PATTERN = + Pattern.compile("[^a-zA-Z0-9_/.]+"); private static final char TAGS_PAIR_DELIMITER = ','; // Delimiter between key-value pairs private static final char TAGS_KEY_VALUE_DELIMITER = '='; // Delimiter between key and value private static final char TAGS_ESCAPE_CHAR = '\\'; - private static final long FLUSH_SHIFT_MS = + private static long FLUSH_SHIFT_MS = (long) (new Random().nextFloat() * (ROLLUP_IN_SECONDS * 1000f)); public static long getDayBucketKey(final @NotNull Calendar timestamp) { @@ -36,11 +44,12 @@ public static long getDayBucketKey(final @NotNull Calendar timestamp) { public static long getTimeBucketKey(final long timestampMs) { final long seconds = timestampMs / 1000; - return (seconds / ROLLUP_IN_SECONDS) * ROLLUP_IN_SECONDS; - } - - public static double getFlushShiftMs() { - return FLUSH_SHIFT_MS; + final long bucketKey = (seconds / ROLLUP_IN_SECONDS) * ROLLUP_IN_SECONDS; + // this will result into timestamps of -9999...9999 to fall into a ~20s bucket + // simply shift the bucket key for negative timestamp values to ensure those two are apart + if (timestampMs >= 0) { + return bucketKey; + } else return bucketKey - 1; } public static long getCutoffTimestampMs(final long nowMs) { @@ -48,11 +57,11 @@ public static long getCutoffTimestampMs(final long nowMs) { } public static @NotNull String sanitizeKey(final @NotNull String input) { - return input.replaceAll(INVALID_KEY_CHARACTERS_PATTERN, "_"); + return INVALID_KEY_CHARACTERS_PATTERN.matcher(input).replaceAll("_"); } public static String sanitizeValue(final @NotNull String input) { - return input.replaceAll(INVALID_VALUE_CHARACTERS_PATTERN, "_"); + return INVALID_VALUE_CHARACTERS_PATTERN.matcher(input).replaceAll("_"); } public static @NotNull String toStatsdType(final @NotNull MetricType type) { @@ -161,7 +170,8 @@ public static void encodeMetrics( final MeasurementUnit unit = metric.getUnit(); final String unitName = (unit != null) ? unit.apiName() : MeasurementUnit.NONE; - writer.append(unitName); + final String sanitizeUnitName = sanitizeUnit(unitName); + writer.append(sanitizeUnitName); for (final @NotNull Object value : metric.getValues()) { writer.append(":"); @@ -193,4 +203,14 @@ public static void encodeMetrics( writer.append("\n"); } } + + @NotNull + public static String sanitizeUnit(@NotNull String unit) { + return INVALID_METRIC_UNIT_CHARACTERS_PATTERN.matcher(unit).replaceAll("_"); + } + + @TestOnly + public static void setFlushShiftMs(long flushShiftMs) { + FLUSH_SHIFT_MS = flushShiftMs; + } } diff --git a/sentry/src/main/java/io/sentry/metrics/NoopMetricAggregator.java b/sentry/src/main/java/io/sentry/metrics/NoopMetricsAggregator.java similarity index 87% rename from sentry/src/main/java/io/sentry/metrics/NoopMetricAggregator.java rename to sentry/src/main/java/io/sentry/metrics/NoopMetricsAggregator.java index 85be81f7c5..f208026100 100644 --- a/sentry/src/main/java/io/sentry/metrics/NoopMetricAggregator.java +++ b/sentry/src/main/java/io/sentry/metrics/NoopMetricsAggregator.java @@ -1,6 +1,6 @@ package io.sentry.metrics; -import io.sentry.IMetricAggregator; +import io.sentry.IMetricsAggregator; import io.sentry.MeasurementUnit; import java.io.IOException; import java.util.Map; @@ -9,11 +9,11 @@ import org.jetbrains.annotations.Nullable; @ApiStatus.Internal -public final class NoopMetricAggregator implements IMetricAggregator { +public final class NoopMetricsAggregator implements IMetricsAggregator { - private static final NoopMetricAggregator instance = new NoopMetricAggregator(); + private static final NoopMetricsAggregator instance = new NoopMetricsAggregator(); - public static NoopMetricAggregator getInstance() { + public static NoopMetricsAggregator getInstance() { return instance; } diff --git a/sentry/src/main/java/io/sentry/metrics/SentryMetric.java b/sentry/src/main/java/io/sentry/metrics/SentryMetric.java index fd0a2be65f..56c14122cf 100644 --- a/sentry/src/main/java/io/sentry/metrics/SentryMetric.java +++ b/sentry/src/main/java/io/sentry/metrics/SentryMetric.java @@ -49,7 +49,7 @@ public SentryMetric(@NotNull Metric metric) { public void serialize(@NotNull ObjectWriter writer, @NotNull ILogger logger) throws IOException { writer.beginObject(); - writer.name(JsonKeys.TYPE).value(MetricHelper.toStatsdType(type)); + writer.name(JsonKeys.TYPE).value(MetricsHelper.toStatsdType(type)); writer.name(JsonKeys.EVENT_ID).value(logger, eventId); writer.name(JsonKeys.NAME).value(key); writer.name(JsonKeys.TIMESTAMP).value((double) timestampMs / 1000.0d); diff --git a/sentry/src/test/java/io/sentry/MetricAggregatorTest.kt b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt similarity index 70% rename from sentry/src/test/java/io/sentry/MetricAggregatorTest.kt rename to sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt index ddcc1b14ff..8bf6180761 100644 --- a/sentry/src/test/java/io/sentry/MetricAggregatorTest.kt +++ b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt @@ -1,7 +1,8 @@ package io.sentry import io.sentry.metrics.IMetricsHub -import io.sentry.metrics.NoopMetricAggregator +import io.sentry.metrics.MetricsHelper +import io.sentry.metrics.NoopMetricsAggregator import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -11,16 +12,18 @@ import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test -class MetricAggregatorTest { +class MetricsAggregatorTest { private val hub = mock() private val logger = mock() - private var currentTimeMillis: Long = 1000 - private var aggregator: IMetricAggregator = NoopMetricAggregator() + private var currentTimeMillis: Long = 0 + private var aggregator: IMetricsAggregator = + NoopMetricsAggregator() @BeforeTest fun setup() { - aggregator = MetricAggregator(hub, logger).also { + MetricsHelper.setFlushShiftMs(0) + aggregator = MetricsAggregator(hub, logger).also { it.setTimeProvider { currentTimeMillis } @@ -31,7 +34,7 @@ class MetricAggregatorTest { @AfterTest fun tearDown() { aggregator.close() - aggregator = NoopMetricAggregator() + aggregator = NoopMetricsAggregator() } @Test @@ -45,17 +48,17 @@ class MetricAggregatorTest { } @Test - fun `flush performs a flush`() { + fun `flush performs a flush when needed`() { // when a metric is emitted - currentTimeMillis = 2000 - aggregator.increment("key", 1.0, null, null, 2001, 1) + currentTimeMillis = 20_000 + aggregator.increment("key", 1.0, null, null, 20_001, 1) // then flush does nothing because there's no data inside the flush interval aggregator.flush(false) verify(hub, never()).captureMetrics(any()) // as times moves on - currentTimeMillis = 20_000 + currentTimeMillis = 30_000 // the metric should be flushed aggregator.flush(false) diff --git a/sentry/src/test/java/io/sentry/metrics/MetricHelperTest.kt b/sentry/src/test/java/io/sentry/metrics/MetricHelperTest.kt deleted file mode 100644 index e2b9f3f6dc..0000000000 --- a/sentry/src/test/java/io/sentry/metrics/MetricHelperTest.kt +++ /dev/null @@ -1,49 +0,0 @@ -package io.sentry.metrics - -import kotlin.test.Test -import kotlin.test.assertEquals - -class MetricHelperTest { - - @Test - fun sanitizeKey() { - assertEquals("foo-bar", MetricHelper.sanitizeKey("foo-bar")) - assertEquals("foo_bar", MetricHelper.sanitizeKey("foo\$\$\$bar")) - assertEquals("fo_-bar", MetricHelper.sanitizeKey("foö-bar")) - } - - @Test - fun sanitizeValue() { - assertEquals("_\$foo", MetricHelper.sanitizeValue("%\$foo")) - assertEquals("blah{}", MetricHelper.sanitizeValue("blah{}")) - assertEquals("sn_wm_n", MetricHelper.sanitizeValue("snöwmän")) - } - - @Test - fun getTimeBucketKey() { - assertEquals( - 10, - MetricHelper.getTimeBucketKey(10_000) - ) - - assertEquals( - 10, - MetricHelper.getTimeBucketKey(10_001) - ) - - assertEquals( - 20, - MetricHelper.getTimeBucketKey(20_000) - ) - - assertEquals( - 20, - MetricHelper.getTimeBucketKey(29_999) - ) - - assertEquals( - 30, - MetricHelper.getTimeBucketKey(30_000) - ) - } -} diff --git a/sentry/src/test/java/io/sentry/metrics/MetricsHelperTest.kt b/sentry/src/test/java/io/sentry/metrics/MetricsHelperTest.kt new file mode 100644 index 0000000000..bab009b79d --- /dev/null +++ b/sentry/src/test/java/io/sentry/metrics/MetricsHelperTest.kt @@ -0,0 +1,62 @@ +package io.sentry.metrics + +import kotlin.test.Test +import kotlin.test.assertEquals + +class MetricsHelperTest { + + @Test + fun sanitizeKey() { + assertEquals("foo-bar", MetricsHelper.sanitizeKey("foo-bar")) + assertEquals("foo_bar", MetricsHelper.sanitizeKey("foo\$\$\$bar")) + assertEquals("fo_-bar", MetricsHelper.sanitizeKey("foö-bar")) + } + + @Test + fun sanitizeValue() { + assertEquals("_\$foo", MetricsHelper.sanitizeValue("%\$foo")) + assertEquals("blah{}", MetricsHelper.sanitizeValue("blah{}")) + assertEquals("sn_wm_n", MetricsHelper.sanitizeValue("snöwmän")) + } + + @Test + fun getTimeBucketKey() { + assertEquals( + 10, + MetricsHelper.getTimeBucketKey(10_000) + ) + + assertEquals( + 10, + MetricsHelper.getTimeBucketKey(10_001) + ) + + assertEquals( + 20, + MetricsHelper.getTimeBucketKey(20_000) + ) + + assertEquals( + 20, + MetricsHelper.getTimeBucketKey(29_999) + ) + + assertEquals( + 30, + MetricsHelper.getTimeBucketKey(30_000) + ) + } + + @Test + fun sanitizeUnit() { + val items = listOf( + "Test123_." to "Test123_.", + "test{value}" to "test_value_", + "test-value" to "test_value" + ) + + for (item in items) { + assertEquals(item.second, MetricsHelper.sanitizeUnit(item.first)) + } + } +} diff --git a/sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt b/sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt index a12db49976..b9a73c364b 100644 --- a/sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt +++ b/sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt @@ -60,9 +60,9 @@ class SetMetricTest { @Test fun toStatsdType() { - assertEquals("c", MetricHelper.toStatsdType(MetricType.Counter)) - assertEquals("g", MetricHelper.toStatsdType(MetricType.Gauge)) - assertEquals("s", MetricHelper.toStatsdType(MetricType.Set)) - assertEquals("d", MetricHelper.toStatsdType(MetricType.Distribution)) + assertEquals("c", MetricsHelper.toStatsdType(MetricType.Counter)) + assertEquals("g", MetricsHelper.toStatsdType(MetricType.Gauge)) + assertEquals("s", MetricsHelper.toStatsdType(MetricType.Set)) + assertEquals("d", MetricsHelper.toStatsdType(MetricType.Distribution)) } } From 72ceeee8cb5bc01bc500a7773f3bf960fc7e5f4b Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Thu, 15 Feb 2024 14:39:36 +0100 Subject: [PATCH 05/26] Add more tests, use cr32 for hashing strings --- .../core/ActivityLifecycleIntegrationTest.kt | 2 +- .../api/sentry-test-support.api | 2 +- .../src/main/kotlin/io/sentry/test/Mocks.kt | 28 ++- sentry/api/sentry.api | 28 +-- .../java/io/sentry/MetricsAggregator.java | 40 +++- .../java/io/sentry/metrics/MetricsHelper.java | 10 +- .../java/io/sentry/MetricsAggregatorTest.kt | 216 +++++++++++++++++- .../io/sentry/metrics/MetricsHelperTest.kt | 116 +++++++++- 8 files changed, 387 insertions(+), 55 deletions(-) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index ec919c1935..2d20a16ec5 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -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() diff --git a/sentry-test-support/api/sentry-test-support.api b/sentry-test-support/api/sentry-test-support.api index 24192b7e01..ffce23a516 100644 --- a/sentry-test-support/api/sentry-test-support.api +++ b/sentry-test-support/api/sentry-test-support.api @@ -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 ()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; diff --git a/sentry-test-support/src/main/kotlin/io/sentry/test/Mocks.kt b/sentry-test-support/src/main/kotlin/io/sentry/test/Mocks.kt index 2ff144fd9b..168f3e6372 100644 --- a/sentry-test-support/src/main/kotlin/io/sentry/test/Mocks.kt +++ b/sentry-test-support/src/main/kotlin/io/sentry/test/Mocks.kt @@ -28,24 +28,40 @@ class ImmediateExecutorService : ISentryExecutorService { class DeferredExecutorService : ISentryExecutorService { - private val runnables = ArrayList() - val scheduledRunnables = ArrayList() + private var runnables = ArrayList() + private var scheduledRunnables = ArrayList() 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 submit(callable: Callable): Future = 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() } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 1cee511dc1..1a2d43e392 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -1021,11 +1021,11 @@ public final class io/sentry/MemoryCollectionData { public final class io/sentry/MetricsAggregator : io/sentry/IMetricsAggregator, java/io/Closeable, java/lang/Runnable { public fun (Lio/sentry/metrics/IMetricsHub;Lio/sentry/ILogger;)V + public fun (Lio/sentry/metrics/IMetricsHub;Lio/sentry/ILogger;Lio/sentry/ISentryExecutorService;)V public fun close ()V public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public fun flush (Z)V public fun gauge (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V - public fun getFlushableBuckets (Z)Ljava/util/Set; public fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public fun run ()V public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;JI)V @@ -3414,16 +3414,6 @@ public abstract class io/sentry/metrics/Metric { public abstract fun getWeight ()I } -public final class io/sentry/metrics/MetricResourceIdentifier { - public fun (Lio/sentry/metrics/MetricType;Ljava/lang/String;Lio/sentry/MeasurementUnit;)V - public fun equals (Ljava/lang/Object;)Z - public fun getKey ()Ljava/lang/String; - public fun getMetricType ()Lio/sentry/metrics/MetricType; - public fun getUnit ()Lio/sentry/MeasurementUnit; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - public final class io/sentry/metrics/MetricType : java/lang/Enum { public static final field Counter Lio/sentry/metrics/MetricType; public static final field Distribution Lio/sentry/metrics/MetricType; @@ -3472,22 +3462,6 @@ public final class io/sentry/metrics/NoopMetricsAggregator : io/sentry/IMetricsA public fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;JI)V } -public final class io/sentry/metrics/SentryMetric : io/sentry/JsonSerializable { - public fun (Lio/sentry/metrics/Metric;)V - public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V -} - -public final class io/sentry/metrics/SentryMetric$JsonKeys { - public static final field EVENT_ID Ljava/lang/String; - public static final field NAME Ljava/lang/String; - public static final field TAGS Ljava/lang/String; - public static final field TIMESTAMP Ljava/lang/String; - public static final field TYPE Ljava/lang/String; - public static final field UNIT Ljava/lang/String; - public static final field VALUE Ljava/lang/String; - public fun ()V -} - public final class io/sentry/metrics/SetMetric : io/sentry/metrics/Metric { public fun (Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun add (D)V diff --git a/sentry/src/main/java/io/sentry/MetricsAggregator.java b/sentry/src/main/java/io/sentry/MetricsAggregator.java index 44904b0807..3ca01b4b3c 100644 --- a/sentry/src/main/java/io/sentry/MetricsAggregator.java +++ b/sentry/src/main/java/io/sentry/MetricsAggregator.java @@ -44,6 +44,7 @@ protected CRC32 initialValue() { private volatile @NotNull ISentryExecutorService executorService; private volatile boolean isClosed = false; + private volatile boolean flushScheduled = false; // The key for this dictionary is the Timestamp for the bucket, rounded down to the nearest // RollupInSeconds... so it @@ -52,10 +53,19 @@ protected CRC32 initialValue() { // each of which has a key that uniquely identifies it within the time period private final NavigableMap> buckets = new ConcurrentSkipListMap<>(); + @TestOnly public MetricsAggregator(final @NotNull IMetricsHub hub, final @NotNull ILogger logger) { + this(hub, logger, NoOpSentryExecutorService.getInstance()); + } + + @TestOnly + public MetricsAggregator( + final @NotNull IMetricsHub hub, + final @NotNull ILogger logger, + final @NotNull ISentryExecutorService executorService) { this.hub = hub; this.logger = logger; - this.executorService = NoOpSentryExecutorService.getInstance(); + this.executorService = executorService; } @Override @@ -149,6 +159,10 @@ private void add( @Nullable Long timestampMs, final int stackLevel) { + if (isClosed) { + return; + } + if (timestampMs == null) { timestampMs = timeProvider.getTimeMillis(); } @@ -187,10 +201,16 @@ private void add( } // spin up real executor service the first time metrics are collected - if (!isClosed && executorService instanceof NoOpSentryExecutorService) { + if (!isClosed && !flushScheduled) { synchronized (this) { - if (!isClosed && executorService instanceof NoOpSentryExecutorService) { - executorService = new SentryExecutorService(); + if (!isClosed && !flushScheduled) { + flushScheduled = true; + // TODO this is probably not a good idea after all + // as it will slow down the first metric emission + // maybe move to constructor? + if (executorService instanceof NoOpSentryExecutorService) { + executorService = new SentryExecutorService(); + } executorService.schedule(this, MetricsHelper.FLUSHER_SLEEP_TIME_MS); } } @@ -225,7 +245,7 @@ public void flush(final boolean force) { } @NotNull - public Set getFlushableBuckets(final boolean force) { + private Set getFlushableBuckets(final boolean force) { if (force) { return buckets.keySet(); } else { @@ -242,8 +262,8 @@ public Set getFlushableBuckets(final boolean force) { private Map getOrAddTimeBucket(final long bucketKey) { @Nullable Map bucket = buckets.get(bucketKey); if (bucket == null) { - // although buckets is thread safe, we still need to synchronize here to avoid overwriting - // buckets + // although buckets is thread safe, we still need to synchronize here to avoid creating + // the same bucket at the same time synchronized (buckets) { bucket = buckets.get(bucketKey); if (bucket == null) { @@ -269,8 +289,10 @@ public void close() throws IOException { public void run() { flush(false); - if (!isClosed) { - executorService.schedule(this, MetricsHelper.FLUSHER_SLEEP_TIME_MS); + synchronized (this) { + if (!isClosed) { + executorService.schedule(this, MetricsHelper.FLUSHER_SLEEP_TIME_MS); + } } } diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java index fda3fb7480..07ae9e7c44 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java @@ -88,7 +88,13 @@ public static String getMetricBucketKey( final @NotNull String typePrefix = toStatsdType(type); final @NotNull String serializedTags = GetTagsKey(tags); - return String.format("%s_%s_%s_%s", typePrefix, metricKey, unit, serializedTags); + final @NotNull String unitName = getUnitName(unit); + return String.format("%s_%s_%s_%s", typePrefix, metricKey, unitName, serializedTags); + } + + @NotNull + private static String getUnitName(final @Nullable MeasurementUnit unit) { + return (unit != null) ? unit.apiName() : MeasurementUnit.NONE; } private static String GetTagsKey(final @Nullable Map tags) { @@ -169,7 +175,7 @@ public static void encodeMetrics( writer.append("@"); final MeasurementUnit unit = metric.getUnit(); - final String unitName = (unit != null) ? unit.apiName() : MeasurementUnit.NONE; + final String unitName = getUnitName(unit); final String sanitizeUnitName = sanitizeUnit(unitName); writer.append(sanitizeUnitName); diff --git a/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt index 8bf6180761..ba1714e567 100644 --- a/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt +++ b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt @@ -2,8 +2,11 @@ package io.sentry import io.sentry.metrics.IMetricsHub import io.sentry.metrics.MetricsHelper +import io.sentry.metrics.MetricsHelperTest import io.sentry.metrics.NoopMetricsAggregator +import io.sentry.test.DeferredExecutorService import org.mockito.kotlin.any +import org.mockito.kotlin.check import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.reset @@ -11,6 +14,9 @@ import org.mockito.kotlin.verify import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue class MetricsAggregatorTest { @@ -19,11 +25,13 @@ class MetricsAggregatorTest { private var currentTimeMillis: Long = 0 private var aggregator: IMetricsAggregator = NoopMetricsAggregator() + private var executorService = DeferredExecutorService() @BeforeTest fun setup() { MetricsHelper.setFlushShiftMs(0) - aggregator = MetricsAggregator(hub, logger).also { + executorService = DeferredExecutorService() + aggregator = MetricsAggregator(hub, logger, executorService).also { it.setTimeProvider { currentTimeMillis } @@ -35,6 +43,7 @@ class MetricsAggregatorTest { fun tearDown() { aggregator.close() aggregator = NoopMetricsAggregator() + executorService.close(0) } @Test @@ -64,4 +73,209 @@ class MetricsAggregatorTest { aggregator.flush(false) verify(hub).captureMetrics(any()) } + + @Test + fun `force flush performs a flushing`() { + // when a metric is emitted + currentTimeMillis = 20_000 + aggregator.increment("key", 1.0, null, null, 20_001, 1) + + // then force flush flushes the metric + aggregator.flush(true) + verify(hub).captureMetrics(any()) + } + + @Test + fun `same metrics are aggregated when in same bucket`() { + currentTimeMillis = 20_000 + + aggregator.increment( + "name", + 1.0, + MeasurementUnit.Custom("apples"), + mapOf("a" to "b"), + 20_001, + 1 + ) + aggregator.increment( + "name", + 1.0, + MeasurementUnit.Custom("apples"), + mapOf("a" to "b"), + 25_001, + 1 + ) + + // then flush does nothing because there's no data inside the flush interval + aggregator.flush(true) + + verify(hub).captureMetrics( + check { + val metrics = MetricsHelperTest.parseMetrics(it.statsd) + assertEquals(1, metrics.size) + assertEquals( + MetricsHelperTest.Companion.StatsDMetric( + 20, + "name", + "apples", + "c", + listOf("2.0"), + mapOf("a" to "b") + ), + metrics[0] + ) + } + ) + } + + @Test + fun `different metrics are not aggregated when in same bucket`() { + // when different metrics are emitted in the same bucket + currentTimeMillis = 20_000 + aggregator.distribution( + "name0", + 1.0, + MeasurementUnit.Custom("unit0"), + mapOf("key0" to "value0"), + 20_001, + 1 + ) + aggregator.increment( + "name0", + 1.0, + MeasurementUnit.Custom("unit0"), + mapOf("key0" to "value0"), + 20_001, + 1 + ) + aggregator.increment( + "name0", + 1.0, + MeasurementUnit.Custom("unit1"), + mapOf("key0" to "value0"), + 20_001, + 1 + ) + aggregator.increment( + "name0", + 1.0, + MeasurementUnit.Custom("unit1"), + mapOf("key1" to "value0"), + 20_001, + 1 + ) + aggregator.increment( + "name0", + 1.0, + MeasurementUnit.Custom("unit1"), + mapOf("key1" to "value1"), + 20_001, + 1 + ) + + aggregator.flush(true) + + // then all of them are emitted separately + verify(hub).captureMetrics( + check { + val metrics = MetricsHelperTest.parseMetrics(it.statsd) + assertEquals(5, metrics.size) + } + ) + } + + @Test + fun `once the aggregator is closed, emissions are ignored`() { + // when aggregator is closed + aggregator.close() + + // and a metric is emitted + currentTimeMillis = 20_000 + aggregator.increment( + "name0", + 1.0, + MeasurementUnit.Custom("unit0"), + mapOf("key0" to "value0"), + 20_001, + 1 + ) + + // then the metric is never captured + aggregator.flush(true) + verify(hub, never()).captureMetrics(any()) + } + + @Test + fun `all metric types can be emitted`() { + currentTimeMillis = 20_000 + aggregator.increment( + "name0", + 1.0, + MeasurementUnit.Custom("unit0"), + mapOf("key0" to "value0"), + 20_001, + 1 + ) + aggregator.distribution( + "name0", + 1.0, + MeasurementUnit.Custom("unit0"), + mapOf("key0" to "value0"), + 20_001, + 1 + ) + aggregator.set( + "name0", + "Hello", + MeasurementUnit.Custom("unit0"), + mapOf("key0" to "value0"), + 20_001, + 1 + ) + aggregator.gauge( + "name0", + 1.0, + MeasurementUnit.Custom("unit0"), + mapOf("key0" to "value0"), + 20_001, + 1 + ) + + aggregator.flush(true) + verify(hub).captureMetrics( + check { + val metrics = MetricsHelperTest.parseMetrics(it.statsd) + assertEquals(4, metrics.size) + } + ) + } + + @Test + fun `flushing gets scheduled and captures metrics`() { + // when nothing happened so far + // then no flushing is scheduled + assertFalse(executorService.hasScheduledRunnables()) + + // when a metric gets emitted + currentTimeMillis = 20_000 + aggregator.increment( + "name0", + 1.0, + MeasurementUnit.Custom("unit0"), + mapOf("key0" to "value0"), + 20_001, + 1 + ) + + // then a flush is scheduled + assertTrue(executorService.hasScheduledRunnables()) + + // after the flush is executed, the metric is captured + currentTimeMillis = 31_000 + executorService.runAll() + verify(hub).captureMetrics(any()) + + // and flushing is scheduled again + assertTrue(executorService.hasScheduledRunnables()) + } } diff --git a/sentry/src/test/java/io/sentry/metrics/MetricsHelperTest.kt b/sentry/src/test/java/io/sentry/metrics/MetricsHelperTest.kt index bab009b79d..f0d382d01d 100644 --- a/sentry/src/test/java/io/sentry/metrics/MetricsHelperTest.kt +++ b/sentry/src/test/java/io/sentry/metrics/MetricsHelperTest.kt @@ -1,10 +1,63 @@ package io.sentry.metrics +import io.sentry.MeasurementUnit import kotlin.test.Test import kotlin.test.assertEquals class MetricsHelperTest { + companion object { + + data class StatsDMetric( + val timestamp: Long?, + val name: String?, + val unit: String?, + val type: String?, + val values: List?, + val tags: Map? + ) + + fun parseMetrics(byteArray: ByteArray): List { + val metrics = mutableListOf() + + val encodedMetrics = byteArray.decodeToString() + for (line in encodedMetrics.split("\n")) { + if (line.isEmpty()) { + continue + } + + val pieces = line.split("|") + val payload = pieces[0].split(":") + + val nameAndUnit = payload[0].split("@", limit = 2) + val name = nameAndUnit[0] + val unit = if (nameAndUnit.size == 2) nameAndUnit[1] else null + + val values = payload.subList(1, payload.size) + val type = pieces[1] + + var timestamp: Long? = null + val tags = mutableMapOf() + + for (piece in pieces.subList(2, pieces.size)) { + if (piece[0] == '#') { + for (pair in piece.substring(1, piece.length).split(",")) { + val (k, v) = pair.split(":", limit = 2) + tags[k] = v + } + } else if (piece[0] == 'T') { + timestamp = piece.substring(1, piece.length).toLong() + } else { + throw IllegalArgumentException("unknown piece $piece") + } + } + metrics.add(StatsDMetric(timestamp, name, unit, type, values, tags)) + } + metrics.sortBy { it.timestamp } + return metrics + } + } + @Test fun sanitizeKey() { assertEquals("foo-bar", MetricsHelper.sanitizeKey("foo-bar")) @@ -19,8 +72,31 @@ class MetricsHelperTest { assertEquals("sn_wm_n", MetricsHelper.sanitizeValue("snöwmän")) } + @Test + fun sanitizeUnit() { + val items = listOf( + "Test123_." to "Test123_.", + "test{value}" to "test_value_", + "test-value" to "test_value" + ) + + for (item in items) { + assertEquals(item.second, MetricsHelper.sanitizeUnit(item.first)) + } + } + @Test fun getTimeBucketKey() { + assertEquals( + 0, + MetricsHelper.getTimeBucketKey(5000) + ) + + assertEquals( + -1, + MetricsHelper.getTimeBucketKey(-5000) + ) + assertEquals( 10, MetricsHelper.getTimeBucketKey(10_000) @@ -48,15 +124,39 @@ class MetricsHelperTest { } @Test - fun sanitizeUnit() { - val items = listOf( - "Test123_." to "Test123_.", - "test{value}" to "test_value_", - "test-value" to "test_value" + fun encode() { + val stringBuilder = StringBuilder() + MetricsHelper.encodeMetrics( + 1000, + listOf( + CounterMetric( + "name", + 1.0, + MeasurementUnit.Custom("oranges"), + mapOf( + "tag1" to "value1", + "tag2" to "value2" + ), + 1000 + ) + ), + stringBuilder ) - for (item in items) { - assertEquals(item.second, MetricsHelper.sanitizeUnit(item.first)) - } + val metrics = parseMetrics(stringBuilder.toString().toByteArray()) + + assertEquals(1, metrics.size) + + assertEquals( + StatsDMetric( + 1000, + "name", + "oranges", + "c", + listOf("1.0"), + mapOf("tag1" to "value1", "tag2" to "value2") + ), + metrics[0] + ) } } From 64445a64d829b1503baff7f40751c2f92c9a692b Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Thu, 15 Feb 2024 15:26:53 +0100 Subject: [PATCH 06/26] Enrich tags, add more tests --- sentry/api/sentry.api | 30 ++- sentry/src/main/java/io/sentry/Hub.java | 2 +- .../java/io/sentry/MetricsAggregator.java | 48 ++++- .../java/io/sentry/MetricsAggregatorTest.kt | 183 +++++++++++++----- 4 files changed, 205 insertions(+), 58 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 1a2d43e392..4a3ed1c82c 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -1020,8 +1020,8 @@ public final class io/sentry/MemoryCollectionData { } public final class io/sentry/MetricsAggregator : io/sentry/IMetricsAggregator, java/io/Closeable, java/lang/Runnable { - public fun (Lio/sentry/metrics/IMetricsHub;Lio/sentry/ILogger;)V - public fun (Lio/sentry/metrics/IMetricsHub;Lio/sentry/ILogger;Lio/sentry/ISentryExecutorService;)V + public fun (Lio/sentry/metrics/IMetricsHub;Lio/sentry/SentryOptions;)V + public fun (Lio/sentry/metrics/IMetricsHub;Lio/sentry/SentryOptions;Lio/sentry/ISentryExecutorService;)V public fun close ()V public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public fun flush (Z)V @@ -3414,6 +3414,16 @@ public abstract class io/sentry/metrics/Metric { public abstract fun getWeight ()I } +public final class io/sentry/metrics/MetricResourceIdentifier { + public fun (Lio/sentry/metrics/MetricType;Ljava/lang/String;Lio/sentry/MeasurementUnit;)V + public fun equals (Ljava/lang/Object;)Z + public fun getKey ()Ljava/lang/String; + public fun getMetricType ()Lio/sentry/metrics/MetricType; + public fun getUnit ()Lio/sentry/MeasurementUnit; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class io/sentry/metrics/MetricType : java/lang/Enum { public static final field Counter Lio/sentry/metrics/MetricType; public static final field Distribution Lio/sentry/metrics/MetricType; @@ -3462,6 +3472,22 @@ public final class io/sentry/metrics/NoopMetricsAggregator : io/sentry/IMetricsA public fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;JI)V } +public final class io/sentry/metrics/SentryMetric : io/sentry/JsonSerializable { + public fun (Lio/sentry/metrics/Metric;)V + public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V +} + +public final class io/sentry/metrics/SentryMetric$JsonKeys { + public static final field EVENT_ID Ljava/lang/String; + public static final field NAME Ljava/lang/String; + public static final field TAGS Ljava/lang/String; + public static final field TIMESTAMP Ljava/lang/String; + public static final field TYPE Ljava/lang/String; + public static final field UNIT Ljava/lang/String; + public static final field VALUE Ljava/lang/String; + public fun ()V +} + public final class io/sentry/metrics/SetMetric : io/sentry/metrics/Metric { public fun (Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun add (D)V diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index 613be675d8..574e2b4680 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -58,7 +58,7 @@ private Hub(final @NotNull SentryOptions options, final @NotNull Stack stack) { // Make sure Hub ready to be used then. this.isEnabled = true; - this.metricAggregator = new MetricsAggregator(this, options.getLogger()); + this.metricAggregator = new MetricsAggregator(this, options); this.metricsApi = new MetricsApi(metricAggregator); } diff --git a/sentry/src/main/java/io/sentry/MetricsAggregator.java b/sentry/src/main/java/io/sentry/MetricsAggregator.java index 3ca01b4b3c..9127e7749f 100644 --- a/sentry/src/main/java/io/sentry/MetricsAggregator.java +++ b/sentry/src/main/java/io/sentry/MetricsAggregator.java @@ -40,6 +40,8 @@ protected CRC32 initialValue() { private final @NotNull IMetricsHub hub; private final @NotNull ILogger logger; + private final @NotNull SentryOptions options; + private @NotNull TimeProvider timeProvider = System::currentTimeMillis; private volatile @NotNull ISentryExecutorService executorService; @@ -53,18 +55,18 @@ protected CRC32 initialValue() { // each of which has a key that uniquely identifies it within the time period private final NavigableMap> buckets = new ConcurrentSkipListMap<>(); - @TestOnly - public MetricsAggregator(final @NotNull IMetricsHub hub, final @NotNull ILogger logger) { - this(hub, logger, NoOpSentryExecutorService.getInstance()); + public MetricsAggregator(final @NotNull IMetricsHub hub, final @NotNull SentryOptions options) { + this(hub, options, NoOpSentryExecutorService.getInstance()); } @TestOnly public MetricsAggregator( final @NotNull IMetricsHub hub, - final @NotNull ILogger logger, + final @NotNull SentryOptions options, final @NotNull ISentryExecutorService executorService) { this.hub = hub; - this.logger = logger; + this.options = options; + this.logger = options.getLogger(); this.executorService = executorService; } @@ -167,19 +169,21 @@ private void add( timestampMs = timeProvider.getTimeMillis(); } + final @NotNull Map enrichedTags = enrichTags(tags); + final @NotNull Metric metric; switch (type) { case Counter: - metric = new CounterMetric(key, value, unit, tags, timestampMs); + metric = new CounterMetric(key, value, unit, enrichedTags, timestampMs); break; case Gauge: - metric = new GaugeMetric(key, value, unit, tags, timestampMs); + metric = new GaugeMetric(key, value, unit, enrichedTags, timestampMs); break; case Distribution: - metric = new DistributionMetric(key, value, unit, tags, timestampMs); + metric = new DistributionMetric(key, value, unit, enrichedTags, timestampMs); break; case Set: - metric = new SetMetric(key, unit, tags, timestampMs); + metric = new SetMetric(key, unit, enrichedTags, timestampMs); //noinspection unchecked metric.add((int) value); break; @@ -190,7 +194,8 @@ private void add( final long timeBucketKey = MetricsHelper.getTimeBucketKey(timestampMs); final @NotNull Map timeBucket = getOrAddTimeBucket(timeBucketKey); - final @NotNull String metricKey = MetricsHelper.getMetricBucketKey(type, key, unit, tags); + final @NotNull String metricKey = + MetricsHelper.getMetricBucketKey(type, key, unit, enrichedTags); synchronized (timeBucket) { @Nullable Metric existingMetric = timeBucket.get(metricKey); if (existingMetric != null) { @@ -217,6 +222,29 @@ private void add( } } + @NotNull + private Map enrichTags(final @Nullable Map tags) { + + final @NotNull Map enrichedTags; + if (tags == null) { + enrichedTags = new HashMap<>(); + } else { + enrichedTags = new HashMap<>(tags); + } + + final @Nullable String release = options.getRelease(); + if (release != null && !enrichedTags.containsKey("release")) { + enrichedTags.put("release", release); + } + + final @Nullable String environment = options.getEnvironment(); + if (environment != null && !enrichedTags.containsKey("environment")) { + enrichedTags.put("environment", environment); + } + + return enrichedTags; + } + @Override public void flush(final boolean force) { final @NotNull Set flushableBuckets = getFlushableBuckets(force); diff --git a/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt index ba1714e567..5824a3a855 100644 --- a/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt +++ b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt @@ -3,15 +3,12 @@ package io.sentry import io.sentry.metrics.IMetricsHub import io.sentry.metrics.MetricsHelper import io.sentry.metrics.MetricsHelperTest -import io.sentry.metrics.NoopMetricsAggregator import io.sentry.test.DeferredExecutorService import org.mockito.kotlin.any import org.mockito.kotlin.check import org.mockito.kotlin.mock import org.mockito.kotlin.never -import org.mockito.kotlin.reset import org.mockito.kotlin.verify -import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals @@ -20,74 +17,78 @@ import kotlin.test.assertTrue class MetricsAggregatorTest { - private val hub = mock() - private val logger = mock() - private var currentTimeMillis: Long = 0 - private var aggregator: IMetricsAggregator = - NoopMetricsAggregator() - private var executorService = DeferredExecutorService() + private class Fixture { + val options = SentryOptions() + val hub = mock() + var currentTimeMillis: Long = 0 + var executorService = DeferredExecutorService() - @BeforeTest - fun setup() { - MetricsHelper.setFlushShiftMs(0) - executorService = DeferredExecutorService() - aggregator = MetricsAggregator(hub, logger, executorService).also { - it.setTimeProvider { - currentTimeMillis + fun getSut(): MetricsAggregator { + return MetricsAggregator(hub, options, executorService).also { + it.setTimeProvider { + currentTimeMillis + } } } - reset(hub) } - @AfterTest - fun tearDown() { - aggregator.close() - aggregator = NoopMetricsAggregator() - executorService.close(0) + private val fixture = Fixture() + + @BeforeTest + fun setup() { + MetricsHelper.setFlushShiftMs(0) } @Test fun `flush is a no-op when there's nothing to flush`() { + val aggregator = fixture.getSut() + // when no metrics are collected // then flush does nothing aggregator.flush(false) - verify(hub, never()).captureMetrics(any()) + verify(fixture.hub, never()).captureMetrics(any()) } @Test fun `flush performs a flush when needed`() { + val aggregator = fixture.getSut() + // when a metric is emitted - currentTimeMillis = 20_000 + fixture.currentTimeMillis = 20_000 aggregator.increment("key", 1.0, null, null, 20_001, 1) // then flush does nothing because there's no data inside the flush interval aggregator.flush(false) - verify(hub, never()).captureMetrics(any()) + verify(fixture.hub, never()).captureMetrics(any()) // as times moves on - currentTimeMillis = 30_000 + fixture.currentTimeMillis = 30_000 // the metric should be flushed aggregator.flush(false) - verify(hub).captureMetrics(any()) + verify(fixture.hub).captureMetrics(any()) } @Test fun `force flush performs a flushing`() { + val aggregator = fixture.getSut() // when a metric is emitted - currentTimeMillis = 20_000 + fixture.currentTimeMillis = 20_000 aggregator.increment("key", 1.0, null, null, 20_001, 1) // then force flush flushes the metric aggregator.flush(true) - verify(hub).captureMetrics(any()) + verify(fixture.hub).captureMetrics(any()) } @Test fun `same metrics are aggregated when in same bucket`() { - currentTimeMillis = 20_000 + val aggregator = fixture.getSut() + fixture.options.environment = "prod" + + fixture.currentTimeMillis = 20_000 aggregator.increment( "name", @@ -109,7 +110,7 @@ class MetricsAggregatorTest { // then flush does nothing because there's no data inside the flush interval aggregator.flush(true) - verify(hub).captureMetrics( + verify(fixture.hub).captureMetrics( check { val metrics = MetricsHelperTest.parseMetrics(it.statsd) assertEquals(1, metrics.size) @@ -120,7 +121,7 @@ class MetricsAggregatorTest { "apples", "c", listOf("2.0"), - mapOf("a" to "b") + mapOf("a" to "b", "environment" to "prod") ), metrics[0] ) @@ -130,8 +131,10 @@ class MetricsAggregatorTest { @Test fun `different metrics are not aggregated when in same bucket`() { + val aggregator = fixture.getSut() + // when different metrics are emitted in the same bucket - currentTimeMillis = 20_000 + fixture.currentTimeMillis = 20_000 aggregator.distribution( "name0", 1.0, @@ -176,7 +179,7 @@ class MetricsAggregatorTest { aggregator.flush(true) // then all of them are emitted separately - verify(hub).captureMetrics( + verify(fixture.hub).captureMetrics( check { val metrics = MetricsHelperTest.parseMetrics(it.statsd) assertEquals(5, metrics.size) @@ -186,11 +189,13 @@ class MetricsAggregatorTest { @Test fun `once the aggregator is closed, emissions are ignored`() { + val aggregator = fixture.getSut() + // when aggregator is closed aggregator.close() // and a metric is emitted - currentTimeMillis = 20_000 + fixture.currentTimeMillis = 20_000 aggregator.increment( "name0", 1.0, @@ -202,12 +207,14 @@ class MetricsAggregatorTest { // then the metric is never captured aggregator.flush(true) - verify(hub, never()).captureMetrics(any()) + verify(fixture.hub, never()).captureMetrics(any()) } @Test fun `all metric types can be emitted`() { - currentTimeMillis = 20_000 + val aggregator = fixture.getSut() + + fixture.currentTimeMillis = 20_000 aggregator.increment( "name0", 1.0, @@ -242,7 +249,7 @@ class MetricsAggregatorTest { ) aggregator.flush(true) - verify(hub).captureMetrics( + verify(fixture.hub).captureMetrics( check { val metrics = MetricsHelperTest.parseMetrics(it.statsd) assertEquals(4, metrics.size) @@ -252,12 +259,14 @@ class MetricsAggregatorTest { @Test fun `flushing gets scheduled and captures metrics`() { + val aggregator = fixture.getSut() + // when nothing happened so far // then no flushing is scheduled - assertFalse(executorService.hasScheduledRunnables()) + assertFalse(fixture.executorService.hasScheduledRunnables()) // when a metric gets emitted - currentTimeMillis = 20_000 + fixture.currentTimeMillis = 20_000 aggregator.increment( "name0", 1.0, @@ -268,14 +277,98 @@ class MetricsAggregatorTest { ) // then a flush is scheduled - assertTrue(executorService.hasScheduledRunnables()) + assertTrue(fixture.executorService.hasScheduledRunnables()) // after the flush is executed, the metric is captured - currentTimeMillis = 31_000 - executorService.runAll() - verify(hub).captureMetrics(any()) + fixture.currentTimeMillis = 31_000 + fixture.executorService.runAll() + verify(fixture.hub).captureMetrics(any()) // and flushing is scheduled again - assertTrue(executorService.hasScheduledRunnables()) + assertTrue(fixture.executorService.hasScheduledRunnables()) + } + + @Test + fun `tags are enriched with environment and release`() { + val aggregator = fixture.getSut() + + fixture.options.release = "1.0" + fixture.options.environment = "prod" + + // when a metric gets emitted + fixture.currentTimeMillis = 20_000 + aggregator.increment( + "name", + 1.0, + MeasurementUnit.Custom("apples"), + mapOf("a" to "b"), + 20_001, + 1 + ) + + aggregator.flush(true) + verify(fixture.hub).captureMetrics( + check { + val metrics = MetricsHelperTest.parseMetrics(it.statsd) + assertEquals( + MetricsHelperTest.Companion.StatsDMetric( + 20, + "name", + "apples", + "c", + listOf("1.0"), + mapOf( + "a" to "b", + "release" to "1.0", + "environment" to "prod" + ) + ), + metrics[0] + ) + } + ) + } + + @Test + fun `existing environment and release tags are not overwritten`() { + val aggregator = fixture.getSut() + + fixture.options.release = "1.0" + fixture.options.environment = "prod" + + // when a metric gets emitted + fixture.currentTimeMillis = 20_000 + aggregator.increment( + "name", + 1.0, + MeasurementUnit.Custom("apples"), + mapOf( + "release" to "2.0", + "environment" to "prod-2" + ), + 20_001, + 1 + ) + + aggregator.flush(true) + verify(fixture.hub).captureMetrics( + check { + val metrics = MetricsHelperTest.parseMetrics(it.statsd) + assertEquals( + MetricsHelperTest.Companion.StatsDMetric( + 20, + "name", + "apples", + "c", + listOf("1.0"), + mapOf( + "release" to "2.0", + "environment" to "prod-2" + ) + ), + metrics[0] + ) + } + ) } } From 1a21cf5c1f330f08655db81f8e668183bb023b0d Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Thu, 15 Feb 2024 15:53:48 +0100 Subject: [PATCH 07/26] Cleanup tests and timing API, remove uneeded threadlocal --- sentry/api/sentry.api | 8 +++--- .../java/io/sentry/IMetricsAggregator.java | 6 ++-- .../java/io/sentry/MetricsAggregator.java | 28 ++++++++----------- .../java/io/sentry/metrics/MetricsApi.java | 5 +--- .../java/io/sentry/metrics/MetricsHelper.java | 4 +-- .../sentry/metrics/NoopMetricsAggregator.java | 3 +- .../java/io/sentry/MetricsAggregatorTest.kt | 21 ++++++++++++-- 7 files changed, 40 insertions(+), 35 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 4a3ed1c82c..b083799b56 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -625,7 +625,7 @@ public abstract interface class io/sentry/IMetricsAggregator : java/io/Closeable public abstract fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public abstract fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public abstract fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;JI)V - public abstract fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;JI)V + public abstract fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;I)V } public abstract interface class io/sentry/IMetricsAggregator$TimingCallback { @@ -1030,7 +1030,7 @@ public final class io/sentry/MetricsAggregator : io/sentry/IMetricsAggregator, j public fun run ()V public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;JI)V - public fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;JI)V + public fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;I)V } public abstract interface class io/sentry/MetricsAggregator$TimeProvider { @@ -3440,7 +3440,7 @@ public final class io/sentry/metrics/MetricsApi { public fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;I)V public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;I)V public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;I)V - public fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;Ljava/lang/Long;I)V + public fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;I)V } public final class io/sentry/metrics/MetricsHelper { @@ -3469,7 +3469,7 @@ public final class io/sentry/metrics/NoopMetricsAggregator : io/sentry/IMetricsA public fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;JI)V - public fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;JI)V + public fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;I)V } public final class io/sentry/metrics/SentryMetric : io/sentry/JsonSerializable { diff --git a/sentry/src/main/java/io/sentry/IMetricsAggregator.java b/sentry/src/main/java/io/sentry/IMetricsAggregator.java index cfb014fe08..822e0ad4de 100644 --- a/sentry/src/main/java/io/sentry/IMetricsAggregator.java +++ b/sentry/src/main/java/io/sentry/IMetricsAggregator.java @@ -107,17 +107,15 @@ void set( * * @param key A unique key identifying the metric * @param callback The code block to measure - * @param unit An optional unit, see {@link MeasurementUnit.Duration} + * @param unit An optional unit, see {@link MeasurementUnit.Duration}, defaults to seconds * @param tags Optional Tags to associate with the metric - * @param timestampMs The time when the metric was emitted * @param stackLevel Optional number of stacks levels to ignore when determining the code location */ void timing( final @NotNull String key, final @NotNull TimingCallback callback, - final @NotNull MeasurementUnit.Duration unit, + final @Nullable MeasurementUnit.Duration unit, final @Nullable Map tags, - final long timestampMs, final int stackLevel); void flush(boolean force); diff --git a/sentry/src/main/java/io/sentry/MetricsAggregator.java b/sentry/src/main/java/io/sentry/MetricsAggregator.java index 9127e7749f..2c04499eab 100644 --- a/sentry/src/main/java/io/sentry/MetricsAggregator.java +++ b/sentry/src/main/java/io/sentry/MetricsAggregator.java @@ -29,15 +29,6 @@ public final class MetricsAggregator implements IMetricsAggregator, Runnable, Cl @SuppressWarnings({"CharsetObjectCanBeUsed"}) private static final Charset UTF8 = Charset.forName("UTF-8"); - @SuppressWarnings("AnonymousHasLambdaAlternative") - private static final ThreadLocal crc32 = - new ThreadLocal() { - @Override - protected CRC32 initialValue() { - return new CRC32(); - } - }; - private final @NotNull IMetricsHub hub; private final @NotNull ILogger logger; private final @NotNull SentryOptions options; @@ -125,8 +116,7 @@ public void set( final byte[] bytes = value.getBytes(UTF8); - final CRC32 crc = crc32.get(); - crc.reset(); + final CRC32 crc = new CRC32(); crc.update(bytes, 0, bytes.length); final int intValue = (int) crc.getValue(); @@ -137,17 +127,19 @@ public void set( public void timing( @NotNull String key, @NotNull TimingCallback callback, - @NotNull MeasurementUnit.Duration unit, + @Nullable MeasurementUnit.Duration unit, @Nullable Map tags, - final long timestampMs, int stackLevel) { - final long start = System.nanoTime(); + final long startMs = timeProvider.getTimeMillis(); + final long startNanos = System.nanoTime(); try { callback.run(); } finally { - final long durationNanos = (System.nanoTime() - start); - final double value = MetricsHelper.convertNanosTo(unit, durationNanos); - add(MetricType.Distribution, key, value, unit, tags, timestampMs, stackLevel + 1); + final MeasurementUnit.Duration durationUnit = + unit != null ? unit : MeasurementUnit.Duration.SECOND; + final long durationNanos = (System.nanoTime() - startNanos); + final double value = MetricsHelper.convertNanosTo(durationUnit, durationNanos); + add(MetricType.Distribution, key, value, durationUnit, tags, startMs, stackLevel + 1); } } @@ -196,6 +188,8 @@ private void add( final @NotNull String metricKey = MetricsHelper.getMetricBucketKey(type, key, unit, enrichedTags); + + // TODO check if we can synchronize only the metric itself synchronized (timeBucket) { @Nullable Metric existingMetric = timeBucket.get(metricKey); if (existingMetric != null) { diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java index 4467c02e20..3fe43ce227 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java @@ -137,7 +137,6 @@ public void set( * @param callback The code block to measure * @param unit An optional unit, see {@link MeasurementUnit.Duration} * @param tags Optional Tags to associate with the metric - * @param timestampMs The time when the metric was emitted * @param stackLevel Optional number of stacks levels to ignore when determining the code location */ public void timing( @@ -145,10 +144,8 @@ public void timing( final @NotNull IMetricsAggregator.TimingCallback callback, final @NotNull MeasurementUnit.Duration unit, final @Nullable Map tags, - final @Nullable Long timestampMs, final int stackLevel) { - final long timestamp = timestampMs != null ? timestampMs : System.currentTimeMillis(); - aggregator.timing(key, callback, unit, tags, timestamp, stackLevel); + aggregator.timing(key, callback, unit, tags, stackLevel); } } diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java index 07ae9e7c44..a676c1287e 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java @@ -174,8 +174,8 @@ public static void encodeMetrics( writer.append(sanitizeKey(metric.getKey())); writer.append("@"); - final MeasurementUnit unit = metric.getUnit(); - final String unitName = getUnitName(unit); + final @Nullable MeasurementUnit unit = metric.getUnit(); + final @NotNull String unitName = getUnitName(unit); final String sanitizeUnitName = sanitizeUnit(unitName); writer.append(sanitizeUnitName); diff --git a/sentry/src/main/java/io/sentry/metrics/NoopMetricsAggregator.java b/sentry/src/main/java/io/sentry/metrics/NoopMetricsAggregator.java index f208026100..09024e1acd 100644 --- a/sentry/src/main/java/io/sentry/metrics/NoopMetricsAggregator.java +++ b/sentry/src/main/java/io/sentry/metrics/NoopMetricsAggregator.java @@ -66,9 +66,8 @@ public void set( public void timing( @NotNull String key, @NotNull TimingCallback callback, - MeasurementUnit.@NotNull Duration unit, + @Nullable MeasurementUnit.Duration unit, @Nullable Map tags, - long timestampMs, int stackLevel) {} @Override diff --git a/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt index 5824a3a855..b8693bc5e9 100644 --- a/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt +++ b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt @@ -232,13 +232,21 @@ class MetricsAggregatorTest { 1 ) aggregator.set( - "name0", + "name0-string", "Hello", MeasurementUnit.Custom("unit0"), mapOf("key0" to "value0"), 20_001, 1 ) + aggregator.set( + "name0-int", + 1234, + MeasurementUnit.Custom("unit0"), + mapOf("key0" to "value0"), + 20_001, + 1 + ) aggregator.gauge( "name0", 1.0, @@ -247,12 +255,21 @@ class MetricsAggregatorTest { 20_001, 1 ) + aggregator.timing( + "name0", + { + Thread.sleep(2) + }, + MeasurementUnit.Duration.SECOND, + mapOf("key0" to "value0"), + 1 + ) aggregator.flush(true) verify(fixture.hub).captureMetrics( check { val metrics = MetricsHelperTest.parseMetrics(it.statsd) - assertEquals(4, metrics.size) + assertEquals(6, metrics.size) } ) } From 27307844cfeb09d10070072d1a466d894372843f Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Thu, 15 Feb 2024 15:56:57 +0100 Subject: [PATCH 08/26] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91fd51e42b..74554bbdd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Add new threshold parameters to monitor config ([#3181](https://github.com/getsentry/sentry-java/pull/3181)) - Report process init time as a span for app start performance ([#3159](https://github.com/getsentry/sentry-java/pull/3159)) +- Add Metrics API ([#3205](https://github.com/getsentry/sentry-java/pull/3205)) ## 7.3.0 From 64b6efcd9092ad86b92f93085f848c6f5617c4e4 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Fri, 16 Feb 2024 20:36:32 +0100 Subject: [PATCH 09/26] Move default tag generation to IMetricsHub, improve dx --- .../sentry/samples/android/MyApplication.java | 2 +- sentry/api/sentry.api | 26 ++ sentry/src/main/java/io/sentry/Hub.java | 12 +- .../java/io/sentry/IMetricsAggregator.java | 2 +- .../java/io/sentry/MetricsAggregator.java | 33 +- .../java/io/sentry/metrics/IMetricsHub.java | 4 + .../java/io/sentry/metrics/MetricsApi.java | 365 +++++++++++++++++- .../sentry/metrics/NoopMetricsAggregator.java | 6 +- .../java/io/sentry/MetricsAggregatorTest.kt | 26 +- 9 files changed, 437 insertions(+), 39 deletions(-) diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java index c52f267286..9a3169fef0 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java @@ -26,7 +26,7 @@ public void onCreate() { // */ // }); - Sentry.metrics().increment("app.start.cold", 1, null, null, 0, 0); + Sentry.metrics().increment("app.start.cold"); } private void strictMode() { diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index e363dc18bd..a1aa3d1738 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -434,6 +434,7 @@ public final class io/sentry/Hub : io/sentry/IHub, io/sentry/metrics/IMetricsHub public fun endSession ()V public fun flush (J)V public fun getBaggage ()Lio/sentry/BaggageHeader; + public fun getDefaultTagsForMetric ()Ljava/util/Map; public fun getLastEventId ()Lio/sentry/protocol/SentryId; public fun getOptions ()Lio/sentry/SentryOptions; public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; @@ -3406,6 +3407,7 @@ public final class io/sentry/metrics/GaugeMetric : io/sentry/metrics/Metric { public abstract interface class io/sentry/metrics/IMetricsHub { public abstract fun captureMetrics (Lio/sentry/metrics/EncodedMetrics;)V + public abstract fun getDefaultTagsForMetric ()Ljava/util/Map; } public abstract class io/sentry/metrics/Metric { @@ -3441,11 +3443,35 @@ public final class io/sentry/metrics/MetricType : java/lang/Enum { public final class io/sentry/metrics/MetricsApi { public fun (Lio/sentry/IMetricsAggregator;)V + public fun distribution (Ljava/lang/String;D)V + public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;)V + public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;)V + public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;I)V + public fun gauge (Ljava/lang/String;D)V + public fun gauge (Ljava/lang/String;DLio/sentry/MeasurementUnit;)V + public fun gauge (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;)V + public fun gauge (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun gauge (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;I)V + public fun increment (Ljava/lang/String;)V + public fun increment (Ljava/lang/String;D)V + public fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;)V + public fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;)V + public fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;I)V + public fun set (Ljava/lang/String;I)V + public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;)V + public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;)V + public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;I)V + public fun set (Ljava/lang/String;Ljava/lang/String;)V + public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;)V + public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;)V + public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;I)V + public fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;)V + public fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;)V + public fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;)V public fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;I)V } diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index 321e00d8fa..216a7a7be0 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -20,6 +20,7 @@ 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; @@ -303,7 +304,8 @@ public void captureMetrics(final @NotNull EncodedMetrics metrics) { final StackItem item = stack.peek(); final SentryEnvelopeItem envelopeItem = SentryEnvelopeItem.fromMetrics(metrics); - // TODO usually the envelope is assembled by the client + // TODO fine this way? + // Usually the envelope is assembled by the client final SentryEnvelopeHeader envelopeHeader = new SentryEnvelopeHeader( new SentryId(), @@ -316,6 +318,14 @@ public void captureMetrics(final @NotNull EncodedMetrics metrics) { } } + @Override + public @NotNull Map getDefaultTagsForMetric() { + final Map tags = new HashMap<>(2); + tags.put("release", options.getRelease()); + tags.put("environment", options.getEnvironment()); + return tags; + } + @Override public void startSession() { if (!isEnabled()) { diff --git a/sentry/src/main/java/io/sentry/IMetricsAggregator.java b/sentry/src/main/java/io/sentry/IMetricsAggregator.java index 822e0ad4de..4e13baa94b 100644 --- a/sentry/src/main/java/io/sentry/IMetricsAggregator.java +++ b/sentry/src/main/java/io/sentry/IMetricsAggregator.java @@ -114,7 +114,7 @@ void set( void timing( final @NotNull String key, final @NotNull TimingCallback callback, - final @Nullable MeasurementUnit.Duration unit, + final @NotNull MeasurementUnit.Duration unit, final @Nullable Map tags, final int stackLevel); diff --git a/sentry/src/main/java/io/sentry/MetricsAggregator.java b/sentry/src/main/java/io/sentry/MetricsAggregator.java index 2c04499eab..3b39664aab 100644 --- a/sentry/src/main/java/io/sentry/MetricsAggregator.java +++ b/sentry/src/main/java/io/sentry/MetricsAggregator.java @@ -12,6 +12,7 @@ import java.io.Closeable; import java.io.IOException; import java.nio.charset.Charset; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.NavigableMap; @@ -31,7 +32,6 @@ public final class MetricsAggregator implements IMetricsAggregator, Runnable, Cl private final @NotNull IMetricsHub hub; private final @NotNull ILogger logger; - private final @NotNull SentryOptions options; private @NotNull TimeProvider timeProvider = System::currentTimeMillis; @@ -56,7 +56,6 @@ public MetricsAggregator( final @NotNull SentryOptions options, final @NotNull ISentryExecutorService executorService) { this.hub = hub; - this.options = options; this.logger = options.getLogger(); this.executorService = executorService; } @@ -127,7 +126,7 @@ public void set( public void timing( @NotNull String key, @NotNull TimingCallback callback, - @Nullable MeasurementUnit.Duration unit, + @NotNull MeasurementUnit.Duration unit, @Nullable Map tags, int stackLevel) { final long startMs = timeProvider.getTimeMillis(); @@ -135,11 +134,9 @@ public void timing( try { callback.run(); } finally { - final MeasurementUnit.Duration durationUnit = - unit != null ? unit : MeasurementUnit.Duration.SECOND; final long durationNanos = (System.nanoTime() - startNanos); - final double value = MetricsHelper.convertNanosTo(durationUnit, durationNanos); - add(MetricType.Distribution, key, value, durationUnit, tags, startMs, stackLevel + 1); + final double value = MetricsHelper.convertNanosTo(unit, durationNanos); + add(MetricType.Distribution, key, value, unit, tags, startMs, stackLevel + 1); } } @@ -218,24 +215,18 @@ private void add( @NotNull private Map enrichTags(final @Nullable Map tags) { - - final @NotNull Map enrichedTags; + final @NotNull Map defaultTags = hub.getDefaultTagsForMetric(); if (tags == null) { - enrichedTags = new HashMap<>(); - } else { - enrichedTags = new HashMap<>(tags); - } - - final @Nullable String release = options.getRelease(); - if (release != null && !enrichedTags.containsKey("release")) { - enrichedTags.put("release", release); + return Collections.unmodifiableMap(defaultTags); } - final @Nullable String environment = options.getEnvironment(); - if (environment != null && !enrichedTags.containsKey("environment")) { - enrichedTags.put("environment", environment); + final @NotNull Map enrichedTags = new HashMap<>(tags); + for (final @NotNull Map.Entry defaultTag : defaultTags.entrySet()) { + final @NotNull String key = defaultTag.getKey(); + if (!enrichedTags.containsKey(key)) { + enrichedTags.put(key, defaultTag.getValue()); + } } - return enrichedTags; } diff --git a/sentry/src/main/java/io/sentry/metrics/IMetricsHub.java b/sentry/src/main/java/io/sentry/metrics/IMetricsHub.java index da28c4c98a..3f41ab5f4e 100644 --- a/sentry/src/main/java/io/sentry/metrics/IMetricsHub.java +++ b/sentry/src/main/java/io/sentry/metrics/IMetricsHub.java @@ -1,5 +1,6 @@ package io.sentry.metrics; +import java.util.Map; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -8,6 +9,9 @@ public interface IMetricsHub { /** Captures one or more metrics to be sent to Sentry. */ void captureMetrics(final @NotNull EncodedMetrics metrics); + @NotNull + Map getDefaultTagsForMetric(); + /** Captures one or more to be sent to Sentry. */ // void captureCodeLocations(final @NotNull CodeLocations codeLocations); diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java index 3fe43ce227..99a549193e 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java @@ -15,6 +15,76 @@ public MetricsApi(final @NotNull IMetricsAggregator aggregator) { this.aggregator = aggregator; } + /** + * Emits an increment of 1.0 for a counter + * + * @param key A unique key identifying the metric + */ + public void increment(final @NotNull String key) { + increment(key, 1.0, null, null, null, 1); + } + + /** + * Emits a Counter metric + * + * @param key A unique key identifying the metric + * @param value The value to be added + */ + public void increment(final @NotNull String key, final double value) { + + increment(key, value, null, null, null, 1); + } + + /** + * 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} + */ + public void increment( + final @NotNull String key, final double value, final @Nullable MeasurementUnit unit) { + + increment(key, value, unit, null, null, 1); + } + + /** + * 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 + */ + public void increment( + final @NotNull String key, + final double value, + final @Nullable MeasurementUnit unit, + final @Nullable Map tags) { + + increment(key, value, unit, tags, null, 1); + } + + /** + * 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. + */ + public void increment( + final @NotNull String key, + final double value, + final @Nullable MeasurementUnit unit, + final @Nullable Map tags, + final @Nullable Long timestampMs) { + + increment(key, value, unit, tags, timestampMs, 1); + } + /** * Emits a Counter metric * @@ -38,6 +108,67 @@ public void increment( aggregator.increment(key, value, unit, tags, timestamp, stackLevel); } + /** + * Emits a Gauge metric + * + * @param key A unique key identifying the metric + * @param value The value to be added + */ + public void gauge(final @NotNull String key, final double value) { + + gauge(key, value, null, null, null, 1); + } + + /** + * 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} + */ + public void gauge( + final @NotNull String key, final double value, final @Nullable MeasurementUnit unit) { + + gauge(key, value, unit, null, null, 1); + } + + /** + * 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 + */ + public void gauge( + final @NotNull String key, + final double value, + final @Nullable MeasurementUnit unit, + final @Nullable Map tags) { + + gauge(key, value, unit, tags, null, 1); + } + + /** + * 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. + */ + public void gauge( + final @NotNull String key, + final double value, + final @Nullable MeasurementUnit unit, + final @Nullable Map tags, + final @Nullable Long timestampMs) { + + gauge(key, value, unit, tags, timestampMs, 1); + } + /** * Emits a Gauge metric * @@ -61,6 +192,67 @@ public void gauge( aggregator.gauge(key, value, unit, tags, timestamp, stackLevel); } + /** + * Emits a Distribution metric + * + * @param key A unique key identifying the metric + * @param value The value to be added + */ + public void distribution(final @NotNull String key, final double value) { + + distribution(key, value, null, null, null, 1); + } + + /** + * 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} + */ + public void distribution( + final @NotNull String key, final double value, final @Nullable MeasurementUnit unit) { + + distribution(key, value, unit, null, null, 1); + } + + /** + * 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 + */ + public void distribution( + final @NotNull String key, + final double value, + final @Nullable MeasurementUnit unit, + final @Nullable Map tags) { + + distribution(key, value, unit, tags, null, 1); + } + + /** + * 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. + */ + public void distribution( + final @NotNull String key, + final double value, + final @Nullable MeasurementUnit unit, + final @Nullable Map tags, + final @Nullable Long timestampMs) { + + distribution(key, value, unit, tags, timestampMs, 1); + } + /** * Emits a Distribution metric * @@ -84,6 +276,67 @@ public void distribution( aggregator.distribution(key, value, unit, tags, timestamp, stackLevel); } + /** + * Emits a Set metric + * + * @param key A unique key identifying the metric + * @param value The value to be added + */ + public void set(final @NotNull String key, final int value) { + + set(key, value, null, null, null, 1); + } + + /** + * 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} + */ + public void set( + final @NotNull String key, final int value, final @Nullable MeasurementUnit unit) { + + set(key, value, unit, null, null, 1); + } + + /** + * 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 + */ + public void set( + final @NotNull String key, + final int value, + final @Nullable MeasurementUnit unit, + final @Nullable Map tags) { + + set(key, value, unit, tags, null, 1); + } + + /** + * 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. + */ + public void set( + final @NotNull String key, + final int value, + final @Nullable MeasurementUnit unit, + final @Nullable Map tags, + final @Nullable Long timestampMs) { + + set(key, value, unit, tags, timestampMs, 1); + } + /** * Emits a Set metric * @@ -107,6 +360,68 @@ public void set( aggregator.set(key, value, unit, tags, timestamp, stackLevel); } + /** + * Emits a Set metric + * + * @param key A unique key identifying the metric + * @param value The value to be added + */ + public void set(final @NotNull String key, final @NotNull String value) { + set(key, value, null, null, null, 1); + } + + /** + * 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} + */ + public void set( + final @NotNull String key, + final @NotNull String value, + final @Nullable MeasurementUnit unit) { + + set(key, value, unit, null, null, 1); + } + + /** + * 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 + */ + public void set( + final @NotNull String key, + final @NotNull String value, + final @Nullable MeasurementUnit unit, + final @Nullable Map tags) { + + set(key, value, unit, tags, null, 1); + } + + /** + * 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. + */ + public void set( + final @NotNull String key, + final @NotNull String value, + final @Nullable MeasurementUnit unit, + final @Nullable Map tags, + final @Nullable Long timestampMs) { + + set(key, value, unit, tags, timestampMs, 1); + } + /** * Emits a Set metric * @@ -130,6 +445,33 @@ public void set( aggregator.set(key, value, unit, tags, timestamp, 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 + */ + public void timing( + final @NotNull String key, final @NotNull IMetricsAggregator.TimingCallback callback) { + + timing(key, callback, null, null, 1); + } + + /** + * 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} + */ + public void timing( + final @NotNull String key, + final @NotNull IMetricsAggregator.TimingCallback callback, + final @NotNull MeasurementUnit.Duration unit) { + + timing(key, callback, unit, null, 1); + } + /** * Emits a distribution with the time it takes to run a given code block. * @@ -137,15 +479,34 @@ public void set( * @param callback The code block to measure * @param unit An optional unit, see {@link MeasurementUnit.Duration} * @param tags Optional Tags to associate with the metric - * @param stackLevel Optional number of stacks levels to ignore when determining the code location */ public void timing( final @NotNull String key, final @NotNull IMetricsAggregator.TimingCallback callback, final @NotNull MeasurementUnit.Duration unit, + final @Nullable Map tags) { + + timing(key, callback, unit, tags, 1); + } + + /** + * 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} + * @param tags Optional Tags to associate with the metric + * @param stackLevel Optional number of stacks levels to ignore when determining the code location + */ + public void timing( + final @NotNull String key, + final @NotNull IMetricsAggregator.TimingCallback callback, + final @Nullable MeasurementUnit.Duration unit, final @Nullable Map tags, final int stackLevel) { - aggregator.timing(key, callback, unit, tags, stackLevel); + final @NotNull MeasurementUnit.Duration durationUnit = + unit != null ? unit : MeasurementUnit.Duration.SECOND; + aggregator.timing(key, callback, durationUnit, tags, stackLevel); } } diff --git a/sentry/src/main/java/io/sentry/metrics/NoopMetricsAggregator.java b/sentry/src/main/java/io/sentry/metrics/NoopMetricsAggregator.java index 09024e1acd..9c3bd37a7c 100644 --- a/sentry/src/main/java/io/sentry/metrics/NoopMetricsAggregator.java +++ b/sentry/src/main/java/io/sentry/metrics/NoopMetricsAggregator.java @@ -66,9 +66,11 @@ public void set( public void timing( @NotNull String key, @NotNull TimingCallback callback, - @Nullable MeasurementUnit.Duration unit, + @NotNull MeasurementUnit.Duration unit, @Nullable Map tags, - int stackLevel) {} + int stackLevel) { + callback.run(); + } @Override public void flush(boolean force) { diff --git a/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt index b8693bc5e9..df44571f43 100644 --- a/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt +++ b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt @@ -9,6 +9,7 @@ import org.mockito.kotlin.check import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals @@ -121,7 +122,7 @@ class MetricsAggregatorTest { "apples", "c", listOf("2.0"), - mapOf("a" to "b", "environment" to "prod") + mapOf("a" to "b") ), metrics[0] ) @@ -308,9 +309,12 @@ class MetricsAggregatorTest { @Test fun `tags are enriched with environment and release`() { val aggregator = fixture.getSut() - - fixture.options.release = "1.0" - fixture.options.environment = "prod" + whenever(fixture.hub.defaultTagsForMetric).thenReturn( + mapOf( + "release" to "1.0", + "environment" to "prod" + ) + ) // when a metric gets emitted fixture.currentTimeMillis = 20_000 @@ -349,9 +353,11 @@ class MetricsAggregatorTest { @Test fun `existing environment and release tags are not overwritten`() { val aggregator = fixture.getSut() - - fixture.options.release = "1.0" - fixture.options.environment = "prod" + whenever(fixture.hub.defaultTagsForMetric).thenReturn( + mapOf( + "defaultTag" to "defaultValue" + ) + ) // when a metric gets emitted fixture.currentTimeMillis = 20_000 @@ -360,8 +366,7 @@ class MetricsAggregatorTest { 1.0, MeasurementUnit.Custom("apples"), mapOf( - "release" to "2.0", - "environment" to "prod-2" + "defaultTag" to "custom-value" ), 20_001, 1 @@ -379,8 +384,7 @@ class MetricsAggregatorTest { "c", listOf("1.0"), mapOf( - "release" to "2.0", - "environment" to "prod-2" + "defaultTag" to "custom-value" ) ), metrics[0] From 02abee8a331bed9ddae5d8abe2fcf192cde84269 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Fri, 16 Feb 2024 21:17:00 +0100 Subject: [PATCH 10/26] Cleanup --- sentry/api/sentry.api | 4 ++-- .../main/java/io/sentry/SentryEnvelopeItem.java | 2 +- .../main/java/io/sentry/metrics/CodeLocations.java | 14 ++++++-------- sentry/src/test/java/io/sentry/SentryClientTest.kt | 4 ++-- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index a1aa3d1738..169a5a60ba 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3370,9 +3370,9 @@ public abstract interface class io/sentry/internal/viewhierarchy/ViewHierarchyEx } public final class io/sentry/metrics/CodeLocations { - public fun (Ljava/util/Calendar;Ljava/util/Map;)V - public fun getDate ()Ljava/util/Calendar; + public fun (DLjava/util/Map;)V public fun getLocations ()Ljava/util/Map; + public fun getTimestamp ()D } public final class io/sentry/metrics/CounterMetric : io/sentry/metrics/Metric { diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index ea9773264e..b372ab3c24 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -185,7 +185,7 @@ public static SentryEnvelopeItem fromCheckIn( SentryEnvelopeItemHeader itemHeader = new SentryEnvelopeItemHeader( - SentryItemType.Statsd, () -> cachedItem.getBytes().length, "application/json", null); + SentryItemType.CheckIn, () -> cachedItem.getBytes().length, "application/json", null); // avoid method refs on Android due to some issues with older AGP setups // noinspection Convert2MethodRef diff --git a/sentry/src/main/java/io/sentry/metrics/CodeLocations.java b/sentry/src/main/java/io/sentry/metrics/CodeLocations.java index c6d4d19003..07dfac2894 100644 --- a/sentry/src/main/java/io/sentry/metrics/CodeLocations.java +++ b/sentry/src/main/java/io/sentry/metrics/CodeLocations.java @@ -1,28 +1,26 @@ package io.sentry.metrics; import io.sentry.protocol.SentryStackFrame; -import java.util.Calendar; import java.util.Map; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; -/** Represents a collection of code locations. */ +/** Represents a collection of code locations, taken at a specific time */ @ApiStatus.Internal public final class CodeLocations { - private final @NotNull Calendar date; + private final double timestamp; private final @NotNull Map locations; public CodeLocations( - final @NotNull Calendar date, - final @NotNull Map locations) { - this.date = date; + final double date, final @NotNull Map locations) { + this.timestamp = date; this.locations = locations; } @NotNull - public Calendar getDate() { - return date; + public double getTimestamp() { + return timestamp; } @NotNull diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 8848fe6cdf..d5c7a54845 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -568,7 +568,7 @@ class SentryClientTest { assertEquals(1, actual.items.count()) val item = actual.items.first() - assertEquals(SentryItemType.Statsd, item.header.type) + assertEquals(SentryItemType.CheckIn, item.header.type) assertEquals("application/json", item.header.contentType) assertEnvelopeItemDataForCheckIn(item) @@ -592,7 +592,7 @@ class SentryClientTest { assertEquals(1, actual.items.count()) val item = actual.items.first() - assertEquals(SentryItemType.Statsd, item.header.type) + assertEquals(SentryItemType.CheckIn, item.header.type) assertEquals("application/json", item.header.contentType) assertEnvelopeItemDataForCheckIn(item) From c371ceaa42da6e6503f866e19fbf61aaf004c4f7 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Fri, 16 Feb 2024 21:19:59 +0100 Subject: [PATCH 11/26] Cleanup --- sentry/src/main/java/io/sentry/metrics/CodeLocations.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry/src/main/java/io/sentry/metrics/CodeLocations.java b/sentry/src/main/java/io/sentry/metrics/CodeLocations.java index 07dfac2894..4125028513 100644 --- a/sentry/src/main/java/io/sentry/metrics/CodeLocations.java +++ b/sentry/src/main/java/io/sentry/metrics/CodeLocations.java @@ -13,12 +13,12 @@ public final class CodeLocations { private final @NotNull Map locations; public CodeLocations( - final double date, final @NotNull Map locations) { - this.timestamp = date; + final double timestamp, + final @NotNull Map locations) { + this.timestamp = timestamp; this.locations = locations; } - @NotNull public double getTimestamp() { return timestamp; } From 3af988ec7b337166f45345f808c9646b9b181798 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Mon, 19 Feb 2024 12:26:14 +0100 Subject: [PATCH 12/26] Fix remove duplicate metricAggregator.close call --- sentry/src/main/java/io/sentry/Hub.java | 2 -- sentry/src/main/java/io/sentry/MetricsAggregator.java | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index 216a7a7be0..c46cd22a43 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -391,8 +391,6 @@ public void close(final boolean isRestarting) { options.getLogger().log(SentryLevel.ERROR, "Error while closing metrics aggregator.", e); } try { - metricAggregator.close(); - for (Integration integration : options.getIntegrations()) { if (integration instanceof Closeable) { try { diff --git a/sentry/src/main/java/io/sentry/MetricsAggregator.java b/sentry/src/main/java/io/sentry/MetricsAggregator.java index 3b39664aab..a0a834b043 100644 --- a/sentry/src/main/java/io/sentry/MetricsAggregator.java +++ b/sentry/src/main/java/io/sentry/MetricsAggregator.java @@ -291,7 +291,7 @@ private Map getOrAddTimeBucket(final long bucketKey) { @Override public void close() throws IOException { synchronized (this) { - this.isClosed = true; + isClosed = true; executorService.close(0); } flush(true); From 3e88d4801cbd7f2308dd6e94a3dbb42031363d36 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 21 Feb 2024 09:02:45 +0100 Subject: [PATCH 13/26] Address PR feedback --- sentry/api/sentry.api | 68 +++--- sentry/src/main/java/io/sentry/Hub.java | 62 ++--- .../java/io/sentry/IMetricsAggregator.java | 6 +- .../main/java/io/sentry/ISentryClient.java | 4 + .../java/io/sentry/MetricsAggregator.java | 87 +++---- .../main/java/io/sentry/NoOpSentryClient.java | 6 + .../src/main/java/io/sentry/SentryClient.java | 36 ++- .../java/io/sentry/SentryEnvelopeItem.java | 7 +- .../main/java/io/sentry/SentryOptions.java | 12 + .../java/io/sentry/metrics/CounterMetric.java | 7 +- .../io/sentry/metrics/DistributionMetric.java | 10 +- .../io/sentry/metrics/EncodedMetrics.java | 18 +- .../java/io/sentry/metrics/GaugeMetric.java | 7 +- .../io/sentry/metrics/IMetricsClient.java | 15 ++ .../java/io/sentry/metrics/IMetricsHub.java | 24 -- .../main/java/io/sentry/metrics/Metric.java | 16 +- .../java/io/sentry/metrics/MetricsApi.java | 55 +++-- .../java/io/sentry/metrics/MetricsHelper.java | 21 ++ .../sentry/metrics/NoopMetricsAggregator.java | 16 +- .../java/io/sentry/metrics/SetMetric.java | 15 +- sentry/src/test/java/io/sentry/HubTest.kt | 15 ++ .../java/io/sentry/MetricsAggregatorTest.kt | 134 +++-------- .../java/io/sentry/metrics/MetricsApiTest.kt | 212 ++++++++++++++++++ 23 files changed, 521 insertions(+), 332 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/metrics/IMetricsClient.java delete mode 100644 sentry/src/main/java/io/sentry/metrics/IMetricsHub.java create mode 100644 sentry/src/test/java/io/sentry/metrics/MetricsApiTest.kt diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 169a5a60ba..928d4c4bf6 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -408,7 +408,7 @@ public final class io/sentry/HttpStatusCodeRange { public fun isInRange (I)Z } -public final class io/sentry/Hub : io/sentry/IHub, io/sentry/metrics/IMetricsHub { +public final class io/sentry/Hub : io/sentry/IHub, io/sentry/metrics/MetricsApi$IMetricsInterface { public fun (Lio/sentry/SentryOptions;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V @@ -421,7 +421,6 @@ public final class io/sentry/Hub : io/sentry/IHub, io/sentry/metrics/IMetricsHub public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; - public fun captureMetrics (Lio/sentry/metrics/EncodedMetrics;)V public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V public fun clearBreadcrumbs ()V @@ -434,8 +433,9 @@ public final class io/sentry/Hub : io/sentry/IHub, io/sentry/metrics/IMetricsHub public fun endSession ()V public fun flush (J)V public fun getBaggage ()Lio/sentry/BaggageHeader; - public fun getDefaultTagsForMetric ()Ljava/util/Map; + public fun getDefaultTagsForMetrics ()Ljava/util/Map; public fun getLastEventId ()Lio/sentry/protocol/SentryId; + public fun getMetricsAggregator ()Lio/sentry/IMetricsAggregator; public fun getOptions ()Lio/sentry/SentryOptions; public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun getSpan ()Lio/sentry/ISpan; @@ -629,11 +629,7 @@ public abstract interface class io/sentry/IMetricsAggregator : java/io/Closeable public abstract fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public abstract fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public abstract fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;JI)V - public abstract fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;I)V -} - -public abstract interface class io/sentry/IMetricsAggregator$TimingCallback { - public abstract fun run ()V + public abstract fun timing (Ljava/lang/String;Ljava/lang/Runnable;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;I)V } public abstract interface class io/sentry/IOptionsObserver { @@ -756,6 +752,7 @@ public abstract interface class io/sentry/ISentryClient { public abstract fun close ()V public abstract fun close (Z)V public abstract fun flush (J)V + public abstract fun getMetricsAggregator ()Lio/sentry/IMetricsAggregator; public abstract fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public abstract fun isEnabled ()Z public fun isHealthy ()Z @@ -1025,8 +1022,8 @@ public final class io/sentry/MemoryCollectionData { } public final class io/sentry/MetricsAggregator : io/sentry/IMetricsAggregator, java/io/Closeable, java/lang/Runnable { - public fun (Lio/sentry/metrics/IMetricsHub;Lio/sentry/SentryOptions;)V - public fun (Lio/sentry/metrics/IMetricsHub;Lio/sentry/SentryOptions;Lio/sentry/ISentryExecutorService;)V + public fun (Lio/sentry/SentryOptions;Lio/sentry/metrics/IMetricsClient;)V + public fun (Lio/sentry/metrics/IMetricsClient;Lio/sentry/ILogger;Lio/sentry/SentryDateProvider;Lio/sentry/ISentryExecutorService;)V public fun close ()V public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public fun flush (Z)V @@ -1035,11 +1032,7 @@ public final class io/sentry/MetricsAggregator : io/sentry/IMetricsAggregator, j public fun run ()V public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;JI)V - public fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;I)V -} - -public abstract interface class io/sentry/MetricsAggregator$TimeProvider { - public abstract fun getTimeMillis ()J + public fun timing (Ljava/lang/String;Ljava/lang/Runnable;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;I)V } public final class io/sentry/MonitorConfig : io/sentry/JsonSerializable, io/sentry/JsonUnknown { @@ -1885,16 +1878,18 @@ public final class io/sentry/SentryBaseEvent$Serializer { public fun serialize (Lio/sentry/SentryBaseEvent;Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V } -public final class io/sentry/SentryClient : io/sentry/ISentryClient { +public final class io/sentry/SentryClient : io/sentry/ISentryClient, io/sentry/metrics/IMetricsClient { public fun captureCheckIn (Lio/sentry/CheckIn;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; + public fun captureMetrics (Lio/sentry/metrics/EncodedMetrics;)Lio/sentry/protocol/SentryId; public fun captureSession (Lio/sentry/Session;Lio/sentry/Hint;)V public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/IScope;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V public fun close ()V public fun close (Z)V public fun flush (J)V + public fun getMetricsAggregator ()Lio/sentry/IMetricsAggregator; public fun getRateLimiter ()Lio/sentry/transport/RateLimiter; public fun isEnabled ()Z public fun isHealthy ()Z @@ -2282,6 +2277,7 @@ public class io/sentry/SentryOptions { public fun isEnableBackpressureHandling ()Z public fun isEnableDeduplication ()Z public fun isEnableExternalConfiguration ()Z + public fun isEnableMetrics ()Z public fun isEnablePrettySerializationOutput ()Z public fun isEnableShutdownHook ()Z public fun isEnableTimeToFullDisplayTracing ()Z @@ -2320,6 +2316,7 @@ public class io/sentry/SentryOptions { public fun setEnableBackpressureHandling (Z)V public fun setEnableDeduplication (Z)V public fun setEnableExternalConfiguration (Z)V + public fun setEnableMetrics (Z)V public fun setEnablePrettySerializationOutput (Z)V public fun setEnableShutdownHook (Z)V public fun setEnableTimeToFullDisplayTracing (Z)V @@ -3378,7 +3375,6 @@ public final class io/sentry/metrics/CodeLocations { public final class io/sentry/metrics/CounterMetric : io/sentry/metrics/Metric { public fun (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun add (D)V - public fun getType ()Lio/sentry/metrics/MetricType; public fun getValue ()D public fun getValues ()Ljava/lang/Iterable; public fun getWeight ()I @@ -3387,36 +3383,33 @@ public final class io/sentry/metrics/CounterMetric : io/sentry/metrics/Metric { public final class io/sentry/metrics/DistributionMetric : io/sentry/metrics/Metric { public fun (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun add (D)V - public fun getType ()Lio/sentry/metrics/MetricType; public fun getValues ()Ljava/lang/Iterable; public fun getWeight ()I } public final class io/sentry/metrics/EncodedMetrics { - public fun (Ljava/lang/String;)V - public fun getStatsd ()[B + public fun (Ljava/util/Map;)V + public fun encode ()[B } public final class io/sentry/metrics/GaugeMetric : io/sentry/metrics/Metric { public fun (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun add (D)V - public fun getType ()Lio/sentry/metrics/MetricType; public fun getValues ()Ljava/lang/Iterable; public fun getWeight ()I } -public abstract interface class io/sentry/metrics/IMetricsHub { - public abstract fun captureMetrics (Lio/sentry/metrics/EncodedMetrics;)V - public abstract fun getDefaultTagsForMetric ()Ljava/util/Map; +public abstract interface class io/sentry/metrics/IMetricsClient { + public abstract fun captureMetrics (Lio/sentry/metrics/EncodedMetrics;)Lio/sentry/protocol/SentryId; } public abstract class io/sentry/metrics/Metric { - public fun (Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V + public fun (Lio/sentry/metrics/MetricType;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public abstract fun add (D)V public fun getKey ()Ljava/lang/String; public fun getTags ()Ljava/util/Map; public fun getTimeStampMs ()Ljava/lang/Long; - public abstract fun getType ()Lio/sentry/metrics/MetricType; + public fun getType ()Lio/sentry/metrics/MetricType; public fun getUnit ()Lio/sentry/MeasurementUnit; public abstract fun getValues ()Ljava/lang/Iterable; public abstract fun getWeight ()I @@ -3442,7 +3435,7 @@ public final class io/sentry/metrics/MetricType : java/lang/Enum { } public final class io/sentry/metrics/MetricsApi { - public fun (Lio/sentry/IMetricsAggregator;)V + public fun (Lio/sentry/metrics/MetricsApi$IMetricsInterface;)V public fun distribution (Ljava/lang/String;D)V public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;)V public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;)V @@ -3469,10 +3462,15 @@ public final class io/sentry/metrics/MetricsApi { public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;)V public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;I)V - public fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;)V - public fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;)V - public fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;)V - public fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;I)V + public fun timing (Ljava/lang/String;Ljava/lang/Runnable;)V + public fun timing (Ljava/lang/String;Ljava/lang/Runnable;Lio/sentry/MeasurementUnit$Duration;)V + public fun timing (Ljava/lang/String;Ljava/lang/Runnable;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;)V + public fun timing (Ljava/lang/String;Ljava/lang/Runnable;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;I)V +} + +public abstract interface class io/sentry/metrics/MetricsApi$IMetricsInterface { + public abstract fun getDefaultTagsForMetrics ()Ljava/util/Map; + public abstract fun getMetricsAggregator ()Lio/sentry/IMetricsAggregator; } public final class io/sentry/metrics/MetricsHelper { @@ -3484,6 +3482,7 @@ public final class io/sentry/metrics/MetricsHelper { public static fun getDayBucketKey (Ljava/util/Calendar;)J public static fun getMetricBucketKey (Lio/sentry/metrics/MetricType;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;)Ljava/lang/String; public static fun getTimeBucketKey (J)J + public static fun mergeTags (Ljava/util/Map;Ljava/util/Map;)Ljava/util/Map; public static fun sanitizeKey (Ljava/lang/String;)Ljava/lang/String; public static fun sanitizeUnit (Ljava/lang/String;)Ljava/lang/String; public static fun sanitizeValue (Ljava/lang/String;)Ljava/lang/String; @@ -3491,17 +3490,19 @@ public final class io/sentry/metrics/MetricsHelper { public static fun toStatsdType (Lio/sentry/metrics/MetricType;)Ljava/lang/String; } -public final class io/sentry/metrics/NoopMetricsAggregator : io/sentry/IMetricsAggregator { +public final class io/sentry/metrics/NoopMetricsAggregator : io/sentry/IMetricsAggregator, io/sentry/metrics/MetricsApi$IMetricsInterface { public fun ()V public fun close ()V public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public fun flush (Z)V public fun gauge (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V + public fun getDefaultTagsForMetrics ()Ljava/util/Map; public static fun getInstance ()Lio/sentry/metrics/NoopMetricsAggregator; + public fun getMetricsAggregator ()Lio/sentry/IMetricsAggregator; public fun increment (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public fun set (Ljava/lang/String;ILio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public fun set (Ljava/lang/String;Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;JI)V - public fun timing (Ljava/lang/String;Lio/sentry/IMetricsAggregator$TimingCallback;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;I)V + public fun timing (Ljava/lang/String;Ljava/lang/Runnable;Lio/sentry/MeasurementUnit$Duration;Ljava/util/Map;I)V } public final class io/sentry/metrics/SentryMetric : io/sentry/JsonSerializable { @@ -3523,7 +3524,6 @@ public final class io/sentry/metrics/SentryMetric$JsonKeys { public final class io/sentry/metrics/SetMetric : io/sentry/metrics/Metric { public fun (Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun add (D)V - public fun getType ()Lio/sentry/metrics/MetricType; public fun getValues ()Ljava/lang/Iterable; public fun getWeight ()I } diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index c46cd22a43..7a8576b9ba 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -4,8 +4,6 @@ import io.sentry.clientreport.DiscardReason; import io.sentry.hints.SessionEndHint; import io.sentry.hints.SessionStartHint; -import io.sentry.metrics.EncodedMetrics; -import io.sentry.metrics.IMetricsHub; import io.sentry.metrics.MetricsApi; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; @@ -28,7 +26,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public final class Hub implements IHub, IMetricsHub { +public final class Hub implements IHub, MetricsApi.IMetricsInterface { private volatile @NotNull SentryId lastEventId; private final @NotNull SentryOptions options; @@ -38,7 +36,6 @@ public final class Hub implements IHub, IMetricsHub { private final @NotNull Map, String>> throwableToSpan = Collections.synchronizedMap(new WeakHashMap<>()); private final @NotNull TransactionPerformanceCollector transactionPerformanceCollector; - private final @NotNull IMetricsAggregator metricAggregator; private final @NotNull MetricsApi metricsApi; public Hub(final @NotNull SentryOptions options) { @@ -59,12 +56,13 @@ private Hub(final @NotNull SentryOptions options, final @NotNull Stack stack) { // Make sure Hub ready to be used then. this.isEnabled = true; - this.metricAggregator = new MetricsAggregator(this, options); - this.metricsApi = new MetricsApi(metricAggregator); + this.metricsApi = new MetricsApi(this); } private Hub(final @NotNull SentryOptions options, final @NotNull StackItem rootStackItem) { this(options, new Stack(options.getLogger(), rootStackItem)); + + Sentry.metrics().increment("hub.init"); } private static void validateOptions(final @NotNull SentryOptions options) { @@ -292,40 +290,6 @@ public void captureUserFeedback(final @NotNull UserFeedback userFeedback) { } } - @Override - public void captureMetrics(final @NotNull EncodedMetrics metrics) { - if (!isEnabled()) { - options - .getLogger() - .log( - SentryLevel.WARNING, - "Instance is disabled and this 'captureMetrics' call is a no-op."); - } else { - final StackItem item = stack.peek(); - final SentryEnvelopeItem envelopeItem = SentryEnvelopeItem.fromMetrics(metrics); - - // TODO fine this way? - // Usually the envelope is assembled by the client - final SentryEnvelopeHeader envelopeHeader = - new SentryEnvelopeHeader( - new SentryId(), - options.getSdkVersion(), - item.getScope().getPropagationContext().traceContext()); - - final SentryEnvelope envelope = - new SentryEnvelope(envelopeHeader, Collections.singleton(envelopeItem)); - item.getClient().captureEnvelope(envelope); - } - } - - @Override - public @NotNull Map getDefaultTagsForMetric() { - final Map tags = new HashMap<>(2); - tags.put("release", options.getRelease()); - tags.put("environment", options.getEnvironment()); - return tags; - } - @Override public void startSession() { if (!isEnabled()) { @@ -385,11 +349,6 @@ public void close(final boolean isRestarting) { .getLogger() .log(SentryLevel.WARNING, "Instance is disabled and this 'close' call is a no-op."); } else { - try { - metricAggregator.close(); - } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, "Error while closing metrics aggregator.", e); - } try { for (Integration integration : options.getIntegrations()) { if (integration instanceof Closeable) { @@ -990,4 +949,17 @@ private IScope buildLocalScope( public @NotNull MetricsApi metrics() { return metricsApi; } + + @Override + public @NotNull IMetricsAggregator getMetricsAggregator() { + return stack.peek().getClient().getMetricsAggregator(); + } + + @Override + public @NotNull Map getDefaultTagsForMetrics() { + final Map tags = new HashMap<>(2); + tags.put("release", options.getRelease()); + tags.put("environment", options.getEnvironment()); + return tags; + } } diff --git a/sentry/src/main/java/io/sentry/IMetricsAggregator.java b/sentry/src/main/java/io/sentry/IMetricsAggregator.java index 4e13baa94b..3c008a30ac 100644 --- a/sentry/src/main/java/io/sentry/IMetricsAggregator.java +++ b/sentry/src/main/java/io/sentry/IMetricsAggregator.java @@ -113,14 +113,10 @@ void set( */ void timing( final @NotNull String key, - final @NotNull TimingCallback callback, + final @NotNull Runnable callback, final @NotNull MeasurementUnit.Duration unit, final @Nullable Map tags, final int stackLevel); void flush(boolean force); - - interface TimingCallback { - void run(); - } } diff --git a/sentry/src/main/java/io/sentry/ISentryClient.java b/sentry/src/main/java/io/sentry/ISentryClient.java index 31b432c560..8685e1db2e 100644 --- a/sentry/src/main/java/io/sentry/ISentryClient.java +++ b/sentry/src/main/java/io/sentry/ISentryClient.java @@ -285,4 +285,8 @@ SentryId captureTransaction( default boolean isHealthy() { return true; } + + @ApiStatus.Internal + @NotNull + IMetricsAggregator getMetricsAggregator(); } diff --git a/sentry/src/main/java/io/sentry/MetricsAggregator.java b/sentry/src/main/java/io/sentry/MetricsAggregator.java index a0a834b043..3f8370ce80 100644 --- a/sentry/src/main/java/io/sentry/MetricsAggregator.java +++ b/sentry/src/main/java/io/sentry/MetricsAggregator.java @@ -4,7 +4,7 @@ import io.sentry.metrics.DistributionMetric; import io.sentry.metrics.EncodedMetrics; import io.sentry.metrics.GaugeMetric; -import io.sentry.metrics.IMetricsHub; +import io.sentry.metrics.IMetricsClient; import io.sentry.metrics.Metric; import io.sentry.metrics.MetricType; import io.sentry.metrics.MetricsHelper; @@ -12,12 +12,12 @@ import java.io.Closeable; import java.io.IOException; import java.nio.charset.Charset; -import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.NavigableMap; import java.util.Set; import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.TimeUnit; import java.util.zip.CRC32; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -30,10 +30,9 @@ public final class MetricsAggregator implements IMetricsAggregator, Runnable, Cl @SuppressWarnings({"CharsetObjectCanBeUsed"}) private static final Charset UTF8 = Charset.forName("UTF-8"); - private final @NotNull IMetricsHub hub; private final @NotNull ILogger logger; - - private @NotNull TimeProvider timeProvider = System::currentTimeMillis; + private final @NotNull IMetricsClient client; + private final @NotNull SentryDateProvider dateProvider; private volatile @NotNull ISentryExecutorService executorService; private volatile boolean isClosed = false; @@ -46,17 +45,24 @@ public final class MetricsAggregator implements IMetricsAggregator, Runnable, Cl // each of which has a key that uniquely identifies it within the time period private final NavigableMap> buckets = new ConcurrentSkipListMap<>(); - public MetricsAggregator(final @NotNull IMetricsHub hub, final @NotNull SentryOptions options) { - this(hub, options, NoOpSentryExecutorService.getInstance()); + public MetricsAggregator( + final @NotNull SentryOptions options, final @NotNull IMetricsClient client) { + this( + client, + options.getLogger(), + options.getDateProvider(), + NoOpSentryExecutorService.getInstance()); } @TestOnly public MetricsAggregator( - final @NotNull IMetricsHub hub, - final @NotNull SentryOptions options, + final @NotNull IMetricsClient client, + final @NotNull ILogger logger, + final @NotNull SentryDateProvider dateProvider, final @NotNull ISentryExecutorService executorService) { - this.hub = hub; - this.logger = options.getLogger(); + this.client = client; + this.logger = logger; + this.dateProvider = dateProvider; this.executorService = executorService; } @@ -125,11 +131,11 @@ public void set( @Override public void timing( @NotNull String key, - @NotNull TimingCallback callback, + @NotNull Runnable callback, @NotNull MeasurementUnit.Duration unit, @Nullable Map tags, int stackLevel) { - final long startMs = timeProvider.getTimeMillis(); + final long startMs = nowMillis(); final long startNanos = System.nanoTime(); try { callback.run(); @@ -155,24 +161,22 @@ private void add( } if (timestampMs == null) { - timestampMs = timeProvider.getTimeMillis(); + timestampMs = nowMillis(); } - final @NotNull Map enrichedTags = enrichTags(tags); - final @NotNull Metric metric; switch (type) { case Counter: - metric = new CounterMetric(key, value, unit, enrichedTags, timestampMs); + metric = new CounterMetric(key, value, unit, tags, timestampMs); break; case Gauge: - metric = new GaugeMetric(key, value, unit, enrichedTags, timestampMs); + metric = new GaugeMetric(key, value, unit, tags, timestampMs); break; case Distribution: - metric = new DistributionMetric(key, value, unit, enrichedTags, timestampMs); + metric = new DistributionMetric(key, value, unit, tags, timestampMs); break; case Set: - metric = new SetMetric(key, unit, enrichedTags, timestampMs); + metric = new SetMetric(key, unit, tags, timestampMs); //noinspection unchecked metric.add((int) value); break; @@ -183,8 +187,7 @@ private void add( final long timeBucketKey = MetricsHelper.getTimeBucketKey(timestampMs); final @NotNull Map timeBucket = getOrAddTimeBucket(timeBucketKey); - final @NotNull String metricKey = - MetricsHelper.getMetricBucketKey(type, key, unit, enrichedTags); + final @NotNull String metricKey = MetricsHelper.getMetricBucketKey(type, key, unit, tags); // TODO check if we can synchronize only the metric itself synchronized (timeBucket) { @@ -213,23 +216,6 @@ private void add( } } - @NotNull - private Map enrichTags(final @Nullable Map tags) { - final @NotNull Map defaultTags = hub.getDefaultTagsForMetric(); - if (tags == null) { - return Collections.unmodifiableMap(defaultTags); - } - - final @NotNull Map enrichedTags = new HashMap<>(tags); - for (final @NotNull Map.Entry defaultTag : defaultTags.entrySet()) { - final @NotNull String key = defaultTag.getKey(); - if (!enrichedTags.containsKey(key)) { - enrichedTags.put(key, defaultTag.getValue()); - } - } - return enrichedTags; - } - @Override public void flush(final boolean force) { final @NotNull Set flushableBuckets = getFlushableBuckets(force); @@ -239,22 +225,23 @@ public void flush(final boolean force) { } logger.log(SentryLevel.DEBUG, "Metrics: flushing " + flushableBuckets.size() + " buckets"); - final @NotNull StringBuilder writer = new StringBuilder(); + final Map> snapshot = new HashMap<>(); + int totalSize = 0; for (long bucketKey : flushableBuckets) { final @Nullable Map metrics = buckets.remove(bucketKey); if (metrics != null) { - MetricsHelper.encodeMetrics(bucketKey, metrics.values(), writer); + totalSize += metrics.size(); + snapshot.put(bucketKey, metrics); } } - if (writer.length() == 0) { + if (totalSize == 0) { logger.log(SentryLevel.DEBUG, "Metrics: only empty buckets found"); return; } logger.log(SentryLevel.DEBUG, "Metrics: capturing metrics"); - final @NotNull EncodedMetrics encodedMetrics = new EncodedMetrics(writer.toString()); - hub.captureMetrics(encodedMetrics); + client.captureMetrics(new EncodedMetrics(snapshot)); } @NotNull @@ -263,8 +250,7 @@ private Set getFlushableBuckets(final boolean force) { return buckets.keySet(); } else { // get all keys, including the cutoff key - final long cutoffTimestampMs = - MetricsHelper.getCutoffTimestampMs(timeProvider.getTimeMillis()); + final long cutoffTimestampMs = MetricsHelper.getCutoffTimestampMs(nowMillis()); final long cutoffKey = MetricsHelper.getTimeBucketKey(cutoffTimestampMs); return buckets.headMap(cutoffKey, true).keySet(); } @@ -309,12 +295,7 @@ public void run() { } } - @TestOnly - void setTimeProvider(final @NotNull TimeProvider timeProvider) { - this.timeProvider = timeProvider; - } - - public interface TimeProvider { - long getTimeMillis(); + private long nowMillis() { + return TimeUnit.NANOSECONDS.toMillis(dateProvider.now().nanoTimestamp()); } } diff --git a/sentry/src/main/java/io/sentry/NoOpSentryClient.java b/sentry/src/main/java/io/sentry/NoOpSentryClient.java index e2ab32c50a..3ae70b4bf5 100644 --- a/sentry/src/main/java/io/sentry/NoOpSentryClient.java +++ b/sentry/src/main/java/io/sentry/NoOpSentryClient.java @@ -1,5 +1,6 @@ package io.sentry; +import io.sentry.metrics.NoopMetricsAggregator; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.transport.RateLimiter; @@ -69,4 +70,9 @@ public SentryId captureEnvelope(@NotNull SentryEnvelope envelope, @Nullable Hint public @Nullable RateLimiter getRateLimiter() { return null; } + + @Override + public @NotNull IMetricsAggregator getMetricsAggregator() { + return NoopMetricsAggregator.getInstance(); + } } diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 57f4559ab3..a1dad7ee72 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -6,6 +6,9 @@ import io.sentry.hints.Backfillable; import io.sentry.hints.DiskFlushNotification; import io.sentry.hints.TransactionEnd; +import io.sentry.metrics.EncodedMetrics; +import io.sentry.metrics.IMetricsClient; +import io.sentry.metrics.NoopMetricsAggregator; import io.sentry.protocol.Contexts; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; @@ -30,7 +33,7 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; -public final class SentryClient implements ISentryClient { +public final class SentryClient implements ISentryClient, IMetricsClient { static final String SENTRY_PROTOCOL_VERSION = "7"; private boolean enabled; @@ -38,8 +41,8 @@ public final class SentryClient implements ISentryClient { private final @NotNull SentryOptions options; private final @NotNull ITransport transport; private final @Nullable SecureRandom random; - private final @NotNull SortBreadcrumbsByDate sortBreadcrumbsByDate = new SortBreadcrumbsByDate(); + private final @NotNull IMetricsAggregator metricsAggregator; @Override public boolean isEnabled() { @@ -59,6 +62,11 @@ public boolean isEnabled() { final RequestDetailsResolver requestDetailsResolver = new RequestDetailsResolver(options); transport = transportFactory.create(options, requestDetailsResolver.resolve()); + metricsAggregator = + options.isEnableMetrics() + ? new MetricsAggregator(options, this) + : NoopMetricsAggregator.getInstance(); + this.random = options.getSampleRate() == null ? null : new SecureRandom(); } @@ -909,7 +917,11 @@ public void close() { @Override public void close(final boolean isRestarting) { options.getLogger().log(SentryLevel.INFO, "Closing SentryClient."); - + try { + metricsAggregator.close(); + } catch (IOException e) { + options.getLogger().log(SentryLevel.WARNING, "Failed to close the metrics aggregator.", e); + } try { flush(isRestarting ? 0 : options.getShutdownTimeoutMillis()); transport.close(isRestarting); @@ -960,6 +972,24 @@ private boolean sample() { return true; } + @Override + public @NotNull IMetricsAggregator getMetricsAggregator() { + return metricsAggregator; + } + + @Override + public @NotNull SentryId captureMetrics(@NotNull EncodedMetrics metrics) { + + final SentryEnvelopeItem envelopeItem = SentryEnvelopeItem.fromMetrics(metrics); + final SentryEnvelopeHeader envelopeHeader = + new SentryEnvelopeHeader(new SentryId(), options.getSdkVersion(), null); + + final SentryEnvelope envelope = + new SentryEnvelope(envelopeHeader, Collections.singleton(envelopeItem)); + final @Nullable SentryId id = captureEnvelope(envelope); + return id != null ? id : SentryId.EMPTY_ID; + } + private static final class SortBreadcrumbsByDate implements Comparator { @SuppressWarnings({"JdkObsolete", "JavaUtilDate"}) diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index b372ab3c24..e1898cdb6e 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -199,12 +199,15 @@ public static SentryEnvelopeItem fromMetrics(final @NotNull EncodedMetrics metri () -> { // avoid method refs on Android due to some issues with older AGP setups //noinspection Convert2MethodRef - return metrics.getStatsd(); + return metrics.encode(); }); final @NotNull SentryEnvelopeItemHeader itemHeader = new SentryEnvelopeItemHeader( - SentryItemType.Statsd, () -> cachedItem.getBytes().length, null, null); + SentryItemType.Statsd, + () -> cachedItem.getBytes().length, + "application/octet-stream", + null); // avoid method refs on Android due to some issues with older AGP setups // noinspection Convert2MethodRef diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index d97b8c79d1..2a04601b39 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -453,6 +453,8 @@ public class SentryOptions { /** Whether to profile app launches, depending on profilesSampler or profilesSampleRate. */ private boolean enableAppStartProfiling = false; + private boolean enableMetrics = false; + /** * Profiling traces rate. 101 hz means 101 traces in 1 second. Defaults to 101 to avoid possible * lockstep sampling. More on @@ -2274,6 +2276,16 @@ public void setSessionFlushTimeoutMillis(final long sessionFlushTimeoutMillis) { this.sessionFlushTimeoutMillis = sessionFlushTimeoutMillis; } + @ApiStatus.Experimental + public boolean isEnableMetrics() { + return enableMetrics; + } + + @ApiStatus.Experimental + public void setEnableMetrics(boolean enableMetrics) { + this.enableMetrics = enableMetrics; + } + /** The BeforeSend callback */ public interface BeforeSendCallback { diff --git a/sentry/src/main/java/io/sentry/metrics/CounterMetric.java b/sentry/src/main/java/io/sentry/metrics/CounterMetric.java index d515024c9f..b73c6438d9 100644 --- a/sentry/src/main/java/io/sentry/metrics/CounterMetric.java +++ b/sentry/src/main/java/io/sentry/metrics/CounterMetric.java @@ -18,7 +18,7 @@ public CounterMetric( final @Nullable MeasurementUnit unit, final @Nullable Map tags, final @NotNull Long timestamp) { - super(key, unit, tags, timestamp); + super(MetricType.Counter, key, unit, tags, timestamp); this.value = value; } @@ -31,11 +31,6 @@ public void add(final double value) { this.value += value; } - @Override - public MetricType getType() { - return MetricType.Counter; - } - @Override public int getWeight() { return 1; diff --git a/sentry/src/main/java/io/sentry/metrics/DistributionMetric.java b/sentry/src/main/java/io/sentry/metrics/DistributionMetric.java index f013018e62..292629395d 100644 --- a/sentry/src/main/java/io/sentry/metrics/DistributionMetric.java +++ b/sentry/src/main/java/io/sentry/metrics/DistributionMetric.java @@ -11,7 +11,7 @@ @ApiStatus.Internal public final class DistributionMetric extends Metric { - private final List values; + private final List values = new ArrayList<>(); public DistributionMetric( final @NotNull String key, @@ -19,8 +19,7 @@ public DistributionMetric( final @Nullable MeasurementUnit unit, final @Nullable Map tags, final @NotNull Long timestamp) { - super(key, unit, tags, timestamp); - this.values = new ArrayList<>(); + super(MetricType.Distribution, key, unit, tags, timestamp); this.values.add(value); } @@ -29,11 +28,6 @@ public void add(final double value) { values.add(value); } - @Override - public MetricType getType() { - return MetricType.Distribution; - } - @Override public int getWeight() { return values.size(); diff --git a/sentry/src/main/java/io/sentry/metrics/EncodedMetrics.java b/sentry/src/main/java/io/sentry/metrics/EncodedMetrics.java index 44fd681a25..eac1a5465d 100644 --- a/sentry/src/main/java/io/sentry/metrics/EncodedMetrics.java +++ b/sentry/src/main/java/io/sentry/metrics/EncodedMetrics.java @@ -1,21 +1,29 @@ package io.sentry.metrics; import java.nio.charset.Charset; +import java.util.Map; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +/** + * EncodedMetrics is a class that represents a collection of aggregated metrics, grouped by buckets. + */ @ApiStatus.Internal public final class EncodedMetrics { @SuppressWarnings({"CharsetObjectCanBeUsed"}) private static final Charset UTF8 = Charset.forName("UTF-8"); - private final @NotNull String statsd; + private final Map> buckets; - public EncodedMetrics(@NotNull String statsd) { - this.statsd = statsd; + public EncodedMetrics(final @NotNull Map> buckets) { + this.buckets = buckets; } - public byte[] getStatsd() { - return statsd.getBytes(UTF8); + public byte[] encode() { + final StringBuilder statsd = new StringBuilder(); + for (Map.Entry> entry : buckets.entrySet()) { + MetricsHelper.encodeMetrics(entry.getKey(), entry.getValue().values(), statsd); + } + return statsd.toString().getBytes(UTF8); } } diff --git a/sentry/src/main/java/io/sentry/metrics/GaugeMetric.java b/sentry/src/main/java/io/sentry/metrics/GaugeMetric.java index 675a75cecb..305b50f584 100644 --- a/sentry/src/main/java/io/sentry/metrics/GaugeMetric.java +++ b/sentry/src/main/java/io/sentry/metrics/GaugeMetric.java @@ -23,7 +23,7 @@ public GaugeMetric( final @Nullable MeasurementUnit unit, final @Nullable Map tags, final @NotNull Long timestamp) { - super(key, unit, tags, timestamp); + super(MetricType.Gauge, key, unit, tags, timestamp); this.last = value; this.min = value; @@ -41,11 +41,6 @@ public void add(final double value) { count++; } - @Override - public MetricType getType() { - return MetricType.Gauge; - } - @Override public int getWeight() { return 5; diff --git a/sentry/src/main/java/io/sentry/metrics/IMetricsClient.java b/sentry/src/main/java/io/sentry/metrics/IMetricsClient.java new file mode 100644 index 0000000000..71b8f730ed --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/IMetricsClient.java @@ -0,0 +1,15 @@ +package io.sentry.metrics; + +import io.sentry.protocol.SentryId; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public interface IMetricsClient { + /** Captures one or more metrics to be sent to Sentry. */ + @NotNull + SentryId captureMetrics(final @NotNull EncodedMetrics metrics); + + /** Captures one or more {@link CodeLocations} to be sent to Sentry. */ + // void captureCodeLocations(final @NotNull CodeLocations codeLocations); +} diff --git a/sentry/src/main/java/io/sentry/metrics/IMetricsHub.java b/sentry/src/main/java/io/sentry/metrics/IMetricsHub.java deleted file mode 100644 index 3f41ab5f4e..0000000000 --- a/sentry/src/main/java/io/sentry/metrics/IMetricsHub.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.sentry.metrics; - -import java.util.Map; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -@ApiStatus.Internal -public interface IMetricsHub { - /** Captures one or more metrics to be sent to Sentry. */ - void captureMetrics(final @NotNull EncodedMetrics metrics); - - @NotNull - Map getDefaultTagsForMetric(); - - /** Captures one or more to be sent to Sentry. */ - // void captureCodeLocations(final @NotNull CodeLocations codeLocations); - - /** - * Starts a child span for the current transaction or, if there is no active transaction, starts a - * new transaction. - */ - // @NotNull ISpan startSpan(final @NotNull String operation, final @NotNull String description); - -} diff --git a/sentry/src/main/java/io/sentry/metrics/Metric.java b/sentry/src/main/java/io/sentry/metrics/Metric.java index 6a6aa52d83..dcc79959c9 100644 --- a/sentry/src/main/java/io/sentry/metrics/Metric.java +++ b/sentry/src/main/java/io/sentry/metrics/Metric.java @@ -10,6 +10,7 @@ @ApiStatus.Internal public abstract class Metric { + private final @NotNull MetricType type; private final @NotNull String key; private final @Nullable MeasurementUnit unit; private final @Nullable Map tags; @@ -25,10 +26,12 @@ public abstract class Metric { * @param timestampMs A time when the metric was emitted. */ public Metric( - @NotNull String key, - @Nullable MeasurementUnit unit, - @Nullable Map tags, - @NotNull Long timestampMs) { + final @NotNull MetricType type, + final @NotNull String key, + final @Nullable MeasurementUnit unit, + final @Nullable Map tags, + final @NotNull Long timestampMs) { + this.type = type; this.key = key; this.unit = unit; this.tags = tags; @@ -38,7 +41,10 @@ public Metric( /** Adds a value to the metric */ public abstract void add(final double value); - public abstract MetricType getType(); + @NotNull + public MetricType getType() { + return type; + } public abstract int getWeight(); diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java index 99a549193e..d500cebe2d 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java @@ -3,15 +3,24 @@ import io.sentry.IMetricsAggregator; import io.sentry.MeasurementUnit; import java.util.Map; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -// TODO: add tons of method overloads to make it delightful to use public final class MetricsApi { - private final @NotNull IMetricsAggregator aggregator; + @ApiStatus.Internal + public interface IMetricsInterface { + @NotNull + IMetricsAggregator getMetricsAggregator(); - public MetricsApi(final @NotNull IMetricsAggregator aggregator) { + @NotNull + Map getDefaultTagsForMetrics(); + } + + private final @NotNull MetricsApi.IMetricsInterface aggregator; + + public MetricsApi(final @NotNull MetricsApi.IMetricsInterface aggregator) { this.aggregator = aggregator; } @@ -31,7 +40,6 @@ public void increment(final @NotNull String key) { * @param value The value to be added */ public void increment(final @NotNull String key, final double value) { - increment(key, value, null, null, null, 1); } @@ -105,7 +113,11 @@ public void increment( final int stackLevel) { final long timestamp = timestampMs != null ? timestampMs : System.currentTimeMillis(); - aggregator.increment(key, value, unit, tags, timestamp, stackLevel); + final @NotNull Map enrichedTags = + MetricsHelper.mergeTags(tags, aggregator.getDefaultTagsForMetrics()); + aggregator + .getMetricsAggregator() + .increment(key, value, unit, enrichedTags, timestamp, stackLevel); } /** @@ -115,7 +127,6 @@ public void increment( * @param value The value to be added */ public void gauge(final @NotNull String key, final double value) { - gauge(key, value, null, null, null, 1); } @@ -128,7 +139,6 @@ public void gauge(final @NotNull String key, final double value) { */ public void gauge( final @NotNull String key, final double value, final @Nullable MeasurementUnit unit) { - gauge(key, value, unit, null, null, 1); } @@ -189,7 +199,9 @@ public void gauge( final int stackLevel) { final long timestamp = timestampMs != null ? timestampMs : System.currentTimeMillis(); - aggregator.gauge(key, value, unit, tags, timestamp, stackLevel); + final @NotNull Map enrichedTags = + MetricsHelper.mergeTags(tags, aggregator.getDefaultTagsForMetrics()); + aggregator.getMetricsAggregator().gauge(key, value, unit, enrichedTags, timestamp, stackLevel); } /** @@ -273,7 +285,11 @@ public void distribution( final int stackLevel) { final long timestamp = timestampMs != null ? timestampMs : System.currentTimeMillis(); - aggregator.distribution(key, value, unit, tags, timestamp, stackLevel); + final @NotNull Map enrichedTags = + MetricsHelper.mergeTags(tags, aggregator.getDefaultTagsForMetrics()); + aggregator + .getMetricsAggregator() + .distribution(key, value, unit, enrichedTags, timestamp, stackLevel); } /** @@ -357,7 +373,9 @@ public void set( final int stackLevel) { final long timestamp = timestampMs != null ? timestampMs : System.currentTimeMillis(); - aggregator.set(key, value, unit, tags, timestamp, stackLevel); + final @NotNull Map enrichedTags = + MetricsHelper.mergeTags(tags, aggregator.getDefaultTagsForMetrics()); + aggregator.getMetricsAggregator().set(key, value, unit, enrichedTags, timestamp, stackLevel); } /** @@ -442,7 +460,9 @@ public void set( final int stackLevel) { final long timestamp = timestampMs != null ? timestampMs : System.currentTimeMillis(); - aggregator.set(key, value, unit, tags, timestamp, stackLevel); + final @NotNull Map enrichedTags = + MetricsHelper.mergeTags(tags, aggregator.getDefaultTagsForMetrics()); + aggregator.getMetricsAggregator().set(key, value, unit, enrichedTags, timestamp, stackLevel); } /** @@ -451,8 +471,7 @@ public void set( * @param key A unique key identifying the metric * @param callback The code block to measure */ - public void timing( - final @NotNull String key, final @NotNull IMetricsAggregator.TimingCallback callback) { + public void timing(final @NotNull String key, final @NotNull Runnable callback) { timing(key, callback, null, null, 1); } @@ -466,7 +485,7 @@ public void timing( */ public void timing( final @NotNull String key, - final @NotNull IMetricsAggregator.TimingCallback callback, + final @NotNull Runnable callback, final @NotNull MeasurementUnit.Duration unit) { timing(key, callback, unit, null, 1); @@ -482,7 +501,7 @@ public void timing( */ public void timing( final @NotNull String key, - final @NotNull IMetricsAggregator.TimingCallback callback, + final @NotNull Runnable callback, final @NotNull MeasurementUnit.Duration unit, final @Nullable Map tags) { @@ -500,13 +519,15 @@ public void timing( */ public void timing( final @NotNull String key, - final @NotNull IMetricsAggregator.TimingCallback callback, + final @NotNull Runnable callback, final @Nullable MeasurementUnit.Duration unit, final @Nullable Map tags, final int stackLevel) { final @NotNull MeasurementUnit.Duration durationUnit = unit != null ? unit : MeasurementUnit.Duration.SECOND; - aggregator.timing(key, callback, durationUnit, tags, stackLevel); + final @NotNull Map enrichedTags = + MetricsHelper.mergeTags(tags, aggregator.getDefaultTagsForMetrics()); + aggregator.getMetricsAggregator().timing(key, callback, durationUnit, enrichedTags, stackLevel); } } diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java index a676c1287e..fa609ea505 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java @@ -3,6 +3,8 @@ import io.sentry.MeasurementUnit; import java.util.Calendar; import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.Random; import java.util.TimeZone; @@ -162,6 +164,9 @@ public static double convertNanosTo( /** * Encodes the metrics * + *

See github.com/statsd/statsd#usage for + * more details about the format + * * @param timestamp The bucket time the metrics belong to, in second resolution * @param metrics The metrics to encode * @param writer The writer to encode the metrics into @@ -215,6 +220,22 @@ public static String sanitizeUnit(@NotNull String unit) { return INVALID_METRIC_UNIT_CHARACTERS_PATTERN.matcher(unit).replaceAll("_"); } + @NotNull + public static Map mergeTags( + final @Nullable Map tags, final @NotNull Map defaultTags) { + if (tags == null) { + return Collections.unmodifiableMap(defaultTags); + } + final @NotNull Map enrichedTags = new HashMap<>(tags); + for (final @NotNull Map.Entry defaultTag : defaultTags.entrySet()) { + final @NotNull String key = defaultTag.getKey(); + if (!enrichedTags.containsKey(key)) { + enrichedTags.put(key, defaultTag.getValue()); + } + } + return enrichedTags; + } + @TestOnly public static void setFlushShiftMs(long flushShiftMs) { FLUSH_SHIFT_MS = flushShiftMs; diff --git a/sentry/src/main/java/io/sentry/metrics/NoopMetricsAggregator.java b/sentry/src/main/java/io/sentry/metrics/NoopMetricsAggregator.java index 9c3bd37a7c..2240c4f497 100644 --- a/sentry/src/main/java/io/sentry/metrics/NoopMetricsAggregator.java +++ b/sentry/src/main/java/io/sentry/metrics/NoopMetricsAggregator.java @@ -3,13 +3,15 @@ import io.sentry.IMetricsAggregator; import io.sentry.MeasurementUnit; import java.io.IOException; +import java.util.Collections; import java.util.Map; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @ApiStatus.Internal -public final class NoopMetricsAggregator implements IMetricsAggregator { +public final class NoopMetricsAggregator + implements IMetricsAggregator, MetricsApi.IMetricsInterface { private static final NoopMetricsAggregator instance = new NoopMetricsAggregator(); @@ -65,7 +67,7 @@ public void set( @Override public void timing( @NotNull String key, - @NotNull TimingCallback callback, + @NotNull Runnable callback, @NotNull MeasurementUnit.Duration unit, @Nullable Map tags, int stackLevel) { @@ -79,4 +81,14 @@ public void flush(boolean force) { @Override public void close() throws IOException {} + + @Override + public @NotNull IMetricsAggregator getMetricsAggregator() { + return this; + } + + @Override + public @NotNull Map getDefaultTagsForMetrics() { + return Collections.emptyMap(); + } } diff --git a/sentry/src/main/java/io/sentry/metrics/SetMetric.java b/sentry/src/main/java/io/sentry/metrics/SetMetric.java index a62ff80bee..b3271a6c4c 100644 --- a/sentry/src/main/java/io/sentry/metrics/SetMetric.java +++ b/sentry/src/main/java/io/sentry/metrics/SetMetric.java @@ -12,27 +12,26 @@ @ApiStatus.Internal public final class SetMetric extends Metric { - private final @NotNull Set values; + private final @NotNull Set values = new HashSet<>(); public SetMetric( final @NotNull String key, final @Nullable MeasurementUnit unit, final @Nullable Map tags, final @NotNull Long timestamp) { - super(key, unit, tags, timestamp); - this.values = new HashSet<>(); + super(MetricType.Set, key, unit, tags, timestamp); } + /** + * Adds a value to the set. Note: the value will be truncated to an integer. + * + * @param value the value to add to the set. + */ @Override public void add(final double value) { values.add((int) value); } - @Override - public MetricType getType() { - return MetricType.Set; - } - @Override public int getWeight() { return values.size(); diff --git a/sentry/src/test/java/io/sentry/HubTest.kt b/sentry/src/test/java/io/sentry/HubTest.kt index d6afaa405b..f01f0bf285 100644 --- a/sentry/src/test/java/io/sentry/HubTest.kt +++ b/sentry/src/test/java/io/sentry/HubTest.kt @@ -1965,6 +1965,21 @@ class HubTest { assertNull(transactionContext) } + @Test + fun `hub provides default metric tags, based on options`() { + val hub = generateHub { + it.environment = "test" + it.release = "1.0" + } as Hub + assertEquals( + mapOf( + "environment" to "test", + "release" to "1.0" + ), + hub.defaultTagsForMetrics + ) + } + private val dsnTest = "https://key@sentry.io/proj" private fun generateHub(optionsConfiguration: Sentry.OptionsConfiguration? = null): IHub { diff --git a/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt index df44571f43..641ae14f8f 100644 --- a/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt +++ b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt @@ -1,6 +1,6 @@ package io.sentry -import io.sentry.metrics.IMetricsHub +import io.sentry.metrics.IMetricsClient import io.sentry.metrics.MetricsHelper import io.sentry.metrics.MetricsHelperTest import io.sentry.test.DeferredExecutorService @@ -9,7 +9,7 @@ import org.mockito.kotlin.check import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever +import java.util.concurrent.TimeUnit import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals @@ -19,17 +19,21 @@ import kotlin.test.assertTrue class MetricsAggregatorTest { private class Fixture { - val options = SentryOptions() - val hub = mock() + val client = mock() + val logger = mock() + val dateProvider = SentryDateProvider { + SentryLongDate(TimeUnit.MILLISECONDS.toNanos(currentTimeMillis)) + } var currentTimeMillis: Long = 0 var executorService = DeferredExecutorService() fun getSut(): MetricsAggregator { - return MetricsAggregator(hub, options, executorService).also { - it.setTimeProvider { - currentTimeMillis - } - } + return MetricsAggregator( + client, + logger, + dateProvider, + executorService + ) } } @@ -49,7 +53,7 @@ class MetricsAggregatorTest { // then flush does nothing aggregator.flush(false) - verify(fixture.hub, never()).captureMetrics(any()) + verify(fixture.client, never()).captureMetrics(any()) } @Test @@ -62,14 +66,14 @@ class MetricsAggregatorTest { // then flush does nothing because there's no data inside the flush interval aggregator.flush(false) - verify(fixture.hub, never()).captureMetrics(any()) + verify(fixture.client, never()).captureMetrics(any()) // as times moves on fixture.currentTimeMillis = 30_000 // the metric should be flushed aggregator.flush(false) - verify(fixture.hub).captureMetrics(any()) + verify(fixture.client).captureMetrics(any()) } @Test @@ -81,13 +85,12 @@ class MetricsAggregatorTest { // then force flush flushes the metric aggregator.flush(true) - verify(fixture.hub).captureMetrics(any()) + verify(fixture.client).captureMetrics(any()) } @Test fun `same metrics are aggregated when in same bucket`() { val aggregator = fixture.getSut() - fixture.options.environment = "prod" fixture.currentTimeMillis = 20_000 @@ -111,9 +114,9 @@ class MetricsAggregatorTest { // then flush does nothing because there's no data inside the flush interval aggregator.flush(true) - verify(fixture.hub).captureMetrics( + verify(fixture.client).captureMetrics( check { - val metrics = MetricsHelperTest.parseMetrics(it.statsd) + val metrics = MetricsHelperTest.parseMetrics(it.encode()) assertEquals(1, metrics.size) assertEquals( MetricsHelperTest.Companion.StatsDMetric( @@ -180,9 +183,9 @@ class MetricsAggregatorTest { aggregator.flush(true) // then all of them are emitted separately - verify(fixture.hub).captureMetrics( + verify(fixture.client).captureMetrics( check { - val metrics = MetricsHelperTest.parseMetrics(it.statsd) + val metrics = MetricsHelperTest.parseMetrics(it.encode()) assertEquals(5, metrics.size) } ) @@ -208,7 +211,7 @@ class MetricsAggregatorTest { // then the metric is never captured aggregator.flush(true) - verify(fixture.hub, never()).captureMetrics(any()) + verify(fixture.client, never()).captureMetrics(any()) } @Test @@ -267,9 +270,9 @@ class MetricsAggregatorTest { ) aggregator.flush(true) - verify(fixture.hub).captureMetrics( + verify(fixture.client).captureMetrics( check { - val metrics = MetricsHelperTest.parseMetrics(it.statsd) + val metrics = MetricsHelperTest.parseMetrics(it.encode()) assertEquals(6, metrics.size) } ) @@ -300,96 +303,9 @@ class MetricsAggregatorTest { // after the flush is executed, the metric is captured fixture.currentTimeMillis = 31_000 fixture.executorService.runAll() - verify(fixture.hub).captureMetrics(any()) + verify(fixture.client).captureMetrics(any()) // and flushing is scheduled again assertTrue(fixture.executorService.hasScheduledRunnables()) } - - @Test - fun `tags are enriched with environment and release`() { - val aggregator = fixture.getSut() - whenever(fixture.hub.defaultTagsForMetric).thenReturn( - mapOf( - "release" to "1.0", - "environment" to "prod" - ) - ) - - // when a metric gets emitted - fixture.currentTimeMillis = 20_000 - aggregator.increment( - "name", - 1.0, - MeasurementUnit.Custom("apples"), - mapOf("a" to "b"), - 20_001, - 1 - ) - - aggregator.flush(true) - verify(fixture.hub).captureMetrics( - check { - val metrics = MetricsHelperTest.parseMetrics(it.statsd) - assertEquals( - MetricsHelperTest.Companion.StatsDMetric( - 20, - "name", - "apples", - "c", - listOf("1.0"), - mapOf( - "a" to "b", - "release" to "1.0", - "environment" to "prod" - ) - ), - metrics[0] - ) - } - ) - } - - @Test - fun `existing environment and release tags are not overwritten`() { - val aggregator = fixture.getSut() - whenever(fixture.hub.defaultTagsForMetric).thenReturn( - mapOf( - "defaultTag" to "defaultValue" - ) - ) - - // when a metric gets emitted - fixture.currentTimeMillis = 20_000 - aggregator.increment( - "name", - 1.0, - MeasurementUnit.Custom("apples"), - mapOf( - "defaultTag" to "custom-value" - ), - 20_001, - 1 - ) - - aggregator.flush(true) - verify(fixture.hub).captureMetrics( - check { - val metrics = MetricsHelperTest.parseMetrics(it.statsd) - assertEquals( - MetricsHelperTest.Companion.StatsDMetric( - 20, - "name", - "apples", - "c", - listOf("1.0"), - mapOf( - "defaultTag" to "custom-value" - ) - ), - metrics[0] - ) - } - ) - } } diff --git a/sentry/src/test/java/io/sentry/metrics/MetricsApiTest.kt b/sentry/src/test/java/io/sentry/metrics/MetricsApiTest.kt new file mode 100644 index 0000000000..14b43465b2 --- /dev/null +++ b/sentry/src/test/java/io/sentry/metrics/MetricsApiTest.kt @@ -0,0 +1,212 @@ +package io.sentry.metrics + +import io.sentry.IMetricsAggregator +import io.sentry.metrics.MetricsApi.IMetricsInterface +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import kotlin.test.Test + +class MetricsApiTest { + + @Test + fun `default timestamp is provided`() { + val aggregator = mock() + val api = MetricsApi(object : IMetricsInterface { + override fun getMetricsAggregator(): IMetricsAggregator { + return aggregator + } + + override fun getDefaultTagsForMetrics(): Map = emptyMap() + }) + + api.increment("name", 1.0, null, null, null) + api.set("name", 1, null, null, null) + api.set("name", "string", null, null, null) + api.gauge("name", 1.0, null, null, null) + api.distribution("name", 1.0, null, null, null) + + verify(aggregator).increment( + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + any(), + anyOrNull() + ) + + verify(aggregator).set( + anyOrNull(), + eq(1), + anyOrNull(), + anyOrNull(), + any(), + anyOrNull() + ) + + verify(aggregator).set( + anyOrNull(), + eq("string"), + anyOrNull(), + anyOrNull(), + any(), + anyOrNull() + ) + + verify(aggregator).gauge( + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + any(), + anyOrNull() + ) + + verify(aggregator).distribution( + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + any(), + anyOrNull() + ) + } + + @Test + fun `timestamp is not overwritten`() { + val aggregator = mock() + val api = MetricsApi(object : IMetricsInterface { + override fun getMetricsAggregator(): IMetricsAggregator { + return aggregator + } + + override fun getDefaultTagsForMetrics(): Map = emptyMap() + }) + + api.increment("name", 1.0, null, null, 1234) + api.set("name", 1, null, null, 1234) + api.set("name", "string", null, null, 1234) + api.gauge("name", 1.0, null, null, 1234) + api.distribution("name", 1.0, null, null, 1234) + + verify(aggregator).increment( + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + eq(1234), + anyOrNull() + ) + + verify(aggregator).set( + anyOrNull(), + eq(1), + anyOrNull(), + anyOrNull(), + eq(1234), + anyOrNull() + ) + + verify(aggregator).set( + anyOrNull(), + eq("string"), + anyOrNull(), + anyOrNull(), + eq(1234), + anyOrNull() + ) + + verify(aggregator).gauge( + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + eq(1234), + anyOrNull() + ) + + verify(aggregator).distribution( + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + eq(1234), + anyOrNull() + ) + } + + @Test + fun `tags are enriched with default tags`() { + val aggregator = mock() + val api = MetricsApi(object : IMetricsInterface { + override fun getMetricsAggregator(): IMetricsAggregator { + return aggregator + } + + override fun getDefaultTagsForMetrics(): Map { + return mapOf( + "release" to "1.0", + "environment" to "prod" + ) + } + }) + api.increment("name", 1.0, null, mapOf("a" to "b")) + + verify(aggregator).increment( + anyOrNull(), + anyOrNull(), + anyOrNull(), + eq( + mapOf( + "a" to "b", + "release" to "1.0", + "environment" to "prod" + ) + ), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `existing environment and release tags are not overwritten`() { + val aggregator = mock() + val api = MetricsApi(object : IMetricsInterface { + override fun getMetricsAggregator(): IMetricsAggregator { + return aggregator + } + + override fun getDefaultTagsForMetrics(): Map { + return mapOf( + "release" to "1.0", + "environment" to "prod" + ) + } + }) + api.increment( + "name", + 1.0, + null, + mapOf( + "release" to "2.0", + "environment" to "dev" + ) + ) + + verify(aggregator).increment( + anyOrNull(), + anyOrNull(), + anyOrNull(), + eq( + mapOf( + "release" to "2.0", + "environment" to "dev" + ) + ), + anyOrNull(), + anyOrNull() + ) + } +} From 01412ab95b61652b9c06676fea3fb0207e0b0e5f Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 21 Feb 2024 09:26:41 +0100 Subject: [PATCH 14/26] Move test into metrics helper --- .../src/test/java/io/sentry/metrics/MetricsHelperTest.kt | 8 ++++++++ sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt | 8 -------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/sentry/src/test/java/io/sentry/metrics/MetricsHelperTest.kt b/sentry/src/test/java/io/sentry/metrics/MetricsHelperTest.kt index f0d382d01d..b91c572c85 100644 --- a/sentry/src/test/java/io/sentry/metrics/MetricsHelperTest.kt +++ b/sentry/src/test/java/io/sentry/metrics/MetricsHelperTest.kt @@ -159,4 +159,12 @@ class MetricsHelperTest { metrics[0] ) } + + @Test + fun toStatsdType() { + assertEquals("c", MetricsHelper.toStatsdType(MetricType.Counter)) + assertEquals("g", MetricsHelper.toStatsdType(MetricType.Gauge)) + assertEquals("s", MetricsHelper.toStatsdType(MetricType.Set)) + assertEquals("d", MetricsHelper.toStatsdType(MetricType.Distribution)) + } } diff --git a/sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt b/sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt index b9a73c364b..6ff209b660 100644 --- a/sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt +++ b/sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt @@ -57,12 +57,4 @@ class SetMetricTest { // weight should be the number of distinct items assertEquals(3, metric.weight) } - - @Test - fun toStatsdType() { - assertEquals("c", MetricsHelper.toStatsdType(MetricType.Counter)) - assertEquals("g", MetricsHelper.toStatsdType(MetricType.Gauge)) - assertEquals("s", MetricsHelper.toStatsdType(MetricType.Set)) - assertEquals("d", MetricsHelper.toStatsdType(MetricType.Distribution)) - } } From 9c67a6c78ea84c9e3f410dec74af225318abcfd3 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Thu, 22 Feb 2024 11:34:44 +0100 Subject: [PATCH 15/26] Rename getValues to serialize to match API spec --- sentry/src/main/java/io/sentry/metrics/CounterMetric.java | 2 +- .../src/main/java/io/sentry/metrics/DistributionMetric.java | 2 +- sentry/src/main/java/io/sentry/metrics/GaugeMetric.java | 2 +- sentry/src/main/java/io/sentry/metrics/Metric.java | 2 +- sentry/src/main/java/io/sentry/metrics/MetricsHelper.java | 2 +- sentry/src/main/java/io/sentry/metrics/SentryMetric.java | 2 +- sentry/src/main/java/io/sentry/metrics/SetMetric.java | 2 +- sentry/src/test/java/io/sentry/metrics/CounterMetricTest.kt | 4 ++-- .../test/java/io/sentry/metrics/DistributionMetricTest.kt | 6 +++--- sentry/src/test/java/io/sentry/metrics/GaugeMetricTest.kt | 4 ++-- sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt | 6 +++--- 11 files changed, 17 insertions(+), 17 deletions(-) diff --git a/sentry/src/main/java/io/sentry/metrics/CounterMetric.java b/sentry/src/main/java/io/sentry/metrics/CounterMetric.java index b73c6438d9..feddfd780c 100644 --- a/sentry/src/main/java/io/sentry/metrics/CounterMetric.java +++ b/sentry/src/main/java/io/sentry/metrics/CounterMetric.java @@ -37,7 +37,7 @@ public int getWeight() { } @Override - public @NotNull Iterable getValues() { + public @NotNull Iterable serialize() { return Collections.singletonList(value); } } diff --git a/sentry/src/main/java/io/sentry/metrics/DistributionMetric.java b/sentry/src/main/java/io/sentry/metrics/DistributionMetric.java index 292629395d..d29c0e2e22 100644 --- a/sentry/src/main/java/io/sentry/metrics/DistributionMetric.java +++ b/sentry/src/main/java/io/sentry/metrics/DistributionMetric.java @@ -34,7 +34,7 @@ public int getWeight() { } @Override - public @NotNull Iterable getValues() { + public @NotNull Iterable serialize() { return values; } } diff --git a/sentry/src/main/java/io/sentry/metrics/GaugeMetric.java b/sentry/src/main/java/io/sentry/metrics/GaugeMetric.java index 305b50f584..dcc78bdcdf 100644 --- a/sentry/src/main/java/io/sentry/metrics/GaugeMetric.java +++ b/sentry/src/main/java/io/sentry/metrics/GaugeMetric.java @@ -47,7 +47,7 @@ public int getWeight() { } @Override - public @NotNull Iterable getValues() { + public @NotNull Iterable serialize() { return Arrays.asList(last, min, max, sum, count); } } diff --git a/sentry/src/main/java/io/sentry/metrics/Metric.java b/sentry/src/main/java/io/sentry/metrics/Metric.java index dcc79959c9..1acb0fe0a9 100644 --- a/sentry/src/main/java/io/sentry/metrics/Metric.java +++ b/sentry/src/main/java/io/sentry/metrics/Metric.java @@ -68,5 +68,5 @@ public Long getTimeStampMs() { return timestampMs; } - public abstract @NotNull Iterable getValues(); + public abstract @NotNull Iterable serialize(); } diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java index fa609ea505..aeaadc4428 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java @@ -184,7 +184,7 @@ public static void encodeMetrics( final String sanitizeUnitName = sanitizeUnit(unitName); writer.append(sanitizeUnitName); - for (final @NotNull Object value : metric.getValues()) { + for (final @NotNull Object value : metric.serialize()) { writer.append(":"); writer.append(value); } diff --git a/sentry/src/main/java/io/sentry/metrics/SentryMetric.java b/sentry/src/main/java/io/sentry/metrics/SentryMetric.java index 56c14122cf..88db999662 100644 --- a/sentry/src/main/java/io/sentry/metrics/SentryMetric.java +++ b/sentry/src/main/java/io/sentry/metrics/SentryMetric.java @@ -42,7 +42,7 @@ public SentryMetric(@NotNull Metric metric) { this.unit = metric.getUnit(); this.tags = metric.getTags(); this.timestampMs = metric.getTimeStampMs(); - this.values = metric.getValues(); + this.values = metric.serialize(); } @Override diff --git a/sentry/src/main/java/io/sentry/metrics/SetMetric.java b/sentry/src/main/java/io/sentry/metrics/SetMetric.java index b3271a6c4c..5aa5a60bea 100644 --- a/sentry/src/main/java/io/sentry/metrics/SetMetric.java +++ b/sentry/src/main/java/io/sentry/metrics/SetMetric.java @@ -38,7 +38,7 @@ public int getWeight() { } @Override - public @NotNull Iterable getValues() { + public @NotNull Iterable serialize() { return values; } } diff --git a/sentry/src/test/java/io/sentry/metrics/CounterMetricTest.kt b/sentry/src/test/java/io/sentry/metrics/CounterMetricTest.kt index 8ca09ae4eb..cbc75e64d3 100644 --- a/sentry/src/test/java/io/sentry/metrics/CounterMetricTest.kt +++ b/sentry/src/test/java/io/sentry/metrics/CounterMetricTest.kt @@ -71,12 +71,12 @@ class CounterMetricTest { System.currentTimeMillis() ) - val values0 = metric.values.toList() + val values0 = metric.serialize().toList() assertEquals(1, values0.size) assertEquals(1.0, values0[0] as Double) metric.add(1.0) - val values1 = metric.values.toList() + val values1 = metric.serialize().toList() assertEquals(1, values1.size) assertEquals(2.0, values1[0] as Double) } diff --git a/sentry/src/test/java/io/sentry/metrics/DistributionMetricTest.kt b/sentry/src/test/java/io/sentry/metrics/DistributionMetricTest.kt index 98c08cf3cc..9fbea650ca 100644 --- a/sentry/src/test/java/io/sentry/metrics/DistributionMetricTest.kt +++ b/sentry/src/test/java/io/sentry/metrics/DistributionMetricTest.kt @@ -14,11 +14,11 @@ class DistributionMetricTest { null, System.currentTimeMillis() ) - assertEquals(listOf(1.0), metric.values.toList()) + assertEquals(listOf(1.0), metric.serialize().toList()) metric.add(1.0) metric.add(2.0) - assertEquals(listOf(1.0, 1.0, 2.0), metric.values.toList()) + assertEquals(listOf(1.0, 1.0, 2.0), metric.serialize().toList()) } @Test @@ -59,7 +59,7 @@ class DistributionMetricTest { ) metric.add(2.0) - val values = metric.values.toList() + val values = metric.serialize().toList() assertEquals(2, values.size) assertEquals(listOf(1.0, 2.0), values) } diff --git a/sentry/src/test/java/io/sentry/metrics/GaugeMetricTest.kt b/sentry/src/test/java/io/sentry/metrics/GaugeMetricTest.kt index f3e83cc734..c6a8a1ab56 100644 --- a/sentry/src/test/java/io/sentry/metrics/GaugeMetricTest.kt +++ b/sentry/src/test/java/io/sentry/metrics/GaugeMetricTest.kt @@ -22,7 +22,7 @@ class GaugeMetricTest { 1.0, 1 ), - metric.values.toList() + metric.serialize().toList() ) metric.add(5.0) @@ -38,7 +38,7 @@ class GaugeMetricTest { 16.0, // sum 6 // count ), - metric.values.toList() + metric.serialize().toList() ) } diff --git a/sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt b/sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt index 6ff209b660..b0065f023b 100644 --- a/sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt +++ b/sentry/src/test/java/io/sentry/metrics/SetMetricTest.kt @@ -14,18 +14,18 @@ class SetMetricTest { null, System.currentTimeMillis() ) - assertTrue(metric.values.toList().isEmpty()) + assertTrue(metric.serialize().toList().isEmpty()) metric.add(1.0) metric.add(2.0) metric.add(3.0) - assertEquals(3, metric.values.toList().size) + assertEquals(3, metric.serialize().toList().size) // when an already existing item is added // size stays the same metric.add(3.0) - assertEquals(3, metric.values.toList().size) + assertEquals(3, metric.serialize().toList().size) } @Test From 6a3be455b0603398ab46d8c2623b60bc1328f714 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Thu, 22 Feb 2024 11:36:17 +0100 Subject: [PATCH 16/26] Add support for force-flushing metrics when weight is too high --- sentry/api/sentry.api | 13 +-- .../java/io/sentry/MetricsAggregator.java | 94 ++++++++++++------- .../java/io/sentry/metrics/MetricsHelper.java | 1 + .../java/io/sentry/MetricsAggregatorTest.kt | 37 +++++++- 4 files changed, 104 insertions(+), 41 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 99b32e8c7f..98777c5d30 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -1023,7 +1023,7 @@ public final class io/sentry/MemoryCollectionData { public final class io/sentry/MetricsAggregator : io/sentry/IMetricsAggregator, java/io/Closeable, java/lang/Runnable { public fun (Lio/sentry/SentryOptions;Lio/sentry/metrics/IMetricsClient;)V - public fun (Lio/sentry/metrics/IMetricsClient;Lio/sentry/ILogger;Lio/sentry/SentryDateProvider;Lio/sentry/ISentryExecutorService;)V + public fun (Lio/sentry/metrics/IMetricsClient;Lio/sentry/ILogger;Lio/sentry/SentryDateProvider;ILio/sentry/ISentryExecutorService;)V public fun close ()V public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public fun flush (Z)V @@ -3395,15 +3395,15 @@ public final class io/sentry/metrics/CounterMetric : io/sentry/metrics/Metric { public fun (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun add (D)V public fun getValue ()D - public fun getValues ()Ljava/lang/Iterable; public fun getWeight ()I + public fun serialize ()Ljava/lang/Iterable; } public final class io/sentry/metrics/DistributionMetric : io/sentry/metrics/Metric { public fun (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun add (D)V - public fun getValues ()Ljava/lang/Iterable; public fun getWeight ()I + public fun serialize ()Ljava/lang/Iterable; } public final class io/sentry/metrics/EncodedMetrics { @@ -3414,8 +3414,8 @@ public final class io/sentry/metrics/EncodedMetrics { public final class io/sentry/metrics/GaugeMetric : io/sentry/metrics/Metric { public fun (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun add (D)V - public fun getValues ()Ljava/lang/Iterable; public fun getWeight ()I + public fun serialize ()Ljava/lang/Iterable; } public abstract interface class io/sentry/metrics/IMetricsClient { @@ -3430,8 +3430,8 @@ public abstract class io/sentry/metrics/Metric { public fun getTimeStampMs ()Ljava/lang/Long; public fun getType ()Lio/sentry/metrics/MetricType; public fun getUnit ()Lio/sentry/MeasurementUnit; - public abstract fun getValues ()Ljava/lang/Iterable; public abstract fun getWeight ()I + public abstract fun serialize ()Ljava/lang/Iterable; } public final class io/sentry/metrics/MetricResourceIdentifier { @@ -3494,6 +3494,7 @@ public abstract interface class io/sentry/metrics/MetricsApi$IMetricsInterface { public final class io/sentry/metrics/MetricsHelper { public static final field FLUSHER_SLEEP_TIME_MS I + public static final field MAX_TOTAL_WEIGHT I public fun ()V public static fun convertNanosTo (Lio/sentry/MeasurementUnit$Duration;J)D public static fun encodeMetrics (JLjava/util/Collection;Ljava/lang/StringBuilder;)V @@ -3543,8 +3544,8 @@ public final class io/sentry/metrics/SentryMetric$JsonKeys { public final class io/sentry/metrics/SetMetric : io/sentry/metrics/Metric { public fun (Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun add (D)V - public fun getValues ()Ljava/lang/Iterable; public fun getWeight ()I + public fun serialize ()Ljava/lang/Iterable; } public final class io/sentry/profilemeasurements/ProfileMeasurement : io/sentry/JsonSerializable, io/sentry/JsonUnknown { diff --git a/sentry/src/main/java/io/sentry/MetricsAggregator.java b/sentry/src/main/java/io/sentry/MetricsAggregator.java index 3f8370ce80..de1224b1ba 100644 --- a/sentry/src/main/java/io/sentry/MetricsAggregator.java +++ b/sentry/src/main/java/io/sentry/MetricsAggregator.java @@ -18,6 +18,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.zip.CRC32; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -44,6 +45,9 @@ public final class MetricsAggregator implements IMetricsAggregator, Runnable, Cl // the metrics, // each of which has a key that uniquely identifies it within the time period private final NavigableMap> buckets = new ConcurrentSkipListMap<>(); + private final AtomicInteger totalBucketsWeight = new AtomicInteger(); + + private final int maxWeight; public MetricsAggregator( final @NotNull SentryOptions options, final @NotNull IMetricsClient client) { @@ -51,6 +55,7 @@ public MetricsAggregator( client, options.getLogger(), options.getDateProvider(), + MetricsHelper.MAX_TOTAL_WEIGHT, NoOpSentryExecutorService.getInstance()); } @@ -59,10 +64,12 @@ public MetricsAggregator( final @NotNull IMetricsClient client, final @NotNull ILogger logger, final @NotNull SentryDateProvider dateProvider, + final int maxWeight, final @NotNull ISentryExecutorService executorService) { this.client = client; this.logger = logger; this.dateProvider = dateProvider; + this.maxWeight = maxWeight; this.executorService = executorService; } @@ -153,49 +160,49 @@ private void add( final double value, @Nullable MeasurementUnit unit, final @Nullable Map tags, - @Nullable Long timestampMs, + @NotNull Long timestampMs, final int stackLevel) { if (isClosed) { return; } - if (timestampMs == null) { - timestampMs = nowMillis(); - } - - final @NotNull Metric metric; - switch (type) { - case Counter: - metric = new CounterMetric(key, value, unit, tags, timestampMs); - break; - case Gauge: - metric = new GaugeMetric(key, value, unit, tags, timestampMs); - break; - case Distribution: - metric = new DistributionMetric(key, value, unit, tags, timestampMs); - break; - case Set: - metric = new SetMetric(key, unit, tags, timestampMs); - //noinspection unchecked - metric.add((int) value); - break; - default: - throw new IllegalArgumentException("Unknown MetricType: " + type.name()); - } - final long timeBucketKey = MetricsHelper.getTimeBucketKey(timestampMs); final @NotNull Map timeBucket = getOrAddTimeBucket(timeBucketKey); final @NotNull String metricKey = MetricsHelper.getMetricBucketKey(type, key, unit, tags); - // TODO check if we can synchronize only the metric itself + // TODO ideally we can synchronize only the metric itself synchronized (timeBucket) { @Nullable Metric existingMetric = timeBucket.get(metricKey); if (existingMetric != null) { + final int oldWeight = existingMetric.getWeight(); existingMetric.add(value); + final int newWeight = existingMetric.getWeight(); + totalBucketsWeight.addAndGet(newWeight - oldWeight); } else { + final @NotNull Metric metric; + switch (type) { + case Counter: + metric = new CounterMetric(key, value, unit, tags, timestampMs); + break; + case Gauge: + metric = new GaugeMetric(key, value, unit, tags, timestampMs); + break; + case Distribution: + metric = new DistributionMetric(key, value, unit, tags, timestampMs); + break; + case Set: + metric = new SetMetric(key, unit, tags, timestampMs); + // sets API is either ints or strings cr32 encoded into ints + // noinspection unchecked + metric.add((int) value); + break; + default: + throw new IllegalArgumentException("Unknown MetricType: " + type.name()); + } timeBucket.put(metricKey, metric); + totalBucketsWeight.addAndGet(metric.getWeight()); } } @@ -217,7 +224,13 @@ private void add( } @Override - public void flush(final boolean force) { + public void flush(boolean force) { + final int totalWeight = buckets.size() + totalBucketsWeight.get(); + if (totalWeight >= maxWeight) { + logger.log(SentryLevel.INFO, "Metrics: total weight exceeded, flushing all buckets"); + force = true; + } + final @NotNull Set flushableBuckets = getFlushableBuckets(force); if (flushableBuckets.isEmpty()) { logger.log(SentryLevel.DEBUG, "Metrics: nothing to flush"); @@ -226,16 +239,21 @@ public void flush(final boolean force) { logger.log(SentryLevel.DEBUG, "Metrics: flushing " + flushableBuckets.size() + " buckets"); final Map> snapshot = new HashMap<>(); - int totalSize = 0; + int numMetrics = 0; for (long bucketKey : flushableBuckets) { - final @Nullable Map metrics = buckets.remove(bucketKey); - if (metrics != null) { - totalSize += metrics.size(); - snapshot.put(bucketKey, metrics); + final @Nullable Map bucket = buckets.remove(bucketKey); + if (bucket != null) { + synchronized (bucket) { + final int weight = getBucketWeight(bucket); + totalBucketsWeight.addAndGet(-weight); + + numMetrics += bucket.size(); + snapshot.put(bucketKey, bucket); + } } } - if (totalSize == 0) { + if (numMetrics == 0) { logger.log(SentryLevel.DEBUG, "Metrics: only empty buckets found"); return; } @@ -244,6 +262,14 @@ public void flush(final boolean force) { client.captureMetrics(new EncodedMetrics(snapshot)); } + private static int getBucketWeight(final @NotNull Map bucket) { + int weight = 0; + for (final @NotNull Metric value : bucket.values()) { + weight += value.getWeight(); + } + return weight; + } + @NotNull private Set getFlushableBuckets(final boolean force) { if (force) { @@ -262,7 +288,7 @@ private Map getOrAddTimeBucket(final long bucketKey) { @Nullable Map bucket = buckets.get(bucketKey); if (bucket == null) { // although buckets is thread safe, we still need to synchronize here to avoid creating - // the same bucket at the same time + // the same bucket at the same time, overwriting each other synchronized (buckets) { bucket = buckets.get(bucketKey); if (bucket == null) { diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java index aeaadc4428..62f9c10f23 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java @@ -17,6 +17,7 @@ @ApiStatus.Internal public final class MetricsHelper { public static final int FLUSHER_SLEEP_TIME_MS = 5000; + public static final int MAX_TOTAL_WEIGHT = 100000; private static final int ROLLUP_IN_SECONDS = 10; private static final Pattern INVALID_KEY_CHARACTERS_PATTERN = diff --git a/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt index 641ae14f8f..b1180044a4 100644 --- a/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt +++ b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt @@ -27,11 +27,12 @@ class MetricsAggregatorTest { var currentTimeMillis: Long = 0 var executorService = DeferredExecutorService() - fun getSut(): MetricsAggregator { + fun getSut(maxWeight: Int = MetricsHelper.MAX_TOTAL_WEIGHT): MetricsAggregator { return MetricsAggregator( client, logger, dateProvider, + maxWeight, executorService ) } @@ -308,4 +309,38 @@ class MetricsAggregatorTest { // and flushing is scheduled again assertTrue(fixture.executorService.hasScheduledRunnables()) } + + @Test + fun `weight is considered for force flushing`() { + // weight is determined by number of buckets + weight of metrics + val aggregator = fixture.getSut(5) + + // when 3 values are emitted + for (i in 0 until 3) { + aggregator.distribution( + "name", + i.toDouble(), + null, + null, + fixture.currentTimeMillis, + 1 + ) + } + // no metrics are captured by the client + aggregator.flush(false) + verify(fixture.client, never()).captureMetrics(any()) + + // once we have 4 values and one bucket = weight of 5 + aggregator.distribution( + "name", + 10.0, + null, + null, + fixture.currentTimeMillis, + 1 + ) + // then flush without force still captures all metrics + aggregator.flush(false) + verify(fixture.client).captureMetrics(any()) + } } From cb0a0028cc2201d76bec3b2d2c2379bb0d3a54f9 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Thu, 22 Feb 2024 13:00:19 +0100 Subject: [PATCH 17/26] Update Changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e41fad69d4..b8ed3a9cfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixes - Fix old profiles deletion on SDK init ([#3216](https://github.com/getsentry/sentry-java/pull/3216)) +- Experimental: Add Metrics API ([#3205](https://github.com/getsentry/sentry-java/pull/3205)) ## 7.4.0 @@ -17,7 +18,6 @@ - Experimental: Add Spotlight integration ([#3166](https://github.com/getsentry/sentry-java/pull/3166)) - For more details about Spotlight head over to https://spotlightjs.com/ - Set `options.isEnableSpotlight = true` to enable Spotlight -- Experimental: Add Metrics API ([#3205](https://github.com/getsentry/sentry-java/pull/3205)) ### Fixes From 07fc4e5ba65233700ee8cc140d9d575b10694f96 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Thu, 22 Feb 2024 15:16:15 +0100 Subject: [PATCH 18/26] Revert "Add support for force-flushing metrics when weight is too high" This reverts commit 6a3be455b0603398ab46d8c2623b60bc1328f714. --- sentry/api/sentry.api | 13 ++- .../java/io/sentry/MetricsAggregator.java | 94 +++++++------------ .../java/io/sentry/metrics/MetricsHelper.java | 1 - .../java/io/sentry/MetricsAggregatorTest.kt | 37 +------- 4 files changed, 41 insertions(+), 104 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 98777c5d30..99b32e8c7f 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -1023,7 +1023,7 @@ public final class io/sentry/MemoryCollectionData { public final class io/sentry/MetricsAggregator : io/sentry/IMetricsAggregator, java/io/Closeable, java/lang/Runnable { public fun (Lio/sentry/SentryOptions;Lio/sentry/metrics/IMetricsClient;)V - public fun (Lio/sentry/metrics/IMetricsClient;Lio/sentry/ILogger;Lio/sentry/SentryDateProvider;ILio/sentry/ISentryExecutorService;)V + public fun (Lio/sentry/metrics/IMetricsClient;Lio/sentry/ILogger;Lio/sentry/SentryDateProvider;Lio/sentry/ISentryExecutorService;)V public fun close ()V public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public fun flush (Z)V @@ -3395,15 +3395,15 @@ public final class io/sentry/metrics/CounterMetric : io/sentry/metrics/Metric { public fun (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun add (D)V public fun getValue ()D + public fun getValues ()Ljava/lang/Iterable; public fun getWeight ()I - public fun serialize ()Ljava/lang/Iterable; } public final class io/sentry/metrics/DistributionMetric : io/sentry/metrics/Metric { public fun (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun add (D)V + public fun getValues ()Ljava/lang/Iterable; public fun getWeight ()I - public fun serialize ()Ljava/lang/Iterable; } public final class io/sentry/metrics/EncodedMetrics { @@ -3414,8 +3414,8 @@ public final class io/sentry/metrics/EncodedMetrics { public final class io/sentry/metrics/GaugeMetric : io/sentry/metrics/Metric { public fun (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun add (D)V + public fun getValues ()Ljava/lang/Iterable; public fun getWeight ()I - public fun serialize ()Ljava/lang/Iterable; } public abstract interface class io/sentry/metrics/IMetricsClient { @@ -3430,8 +3430,8 @@ public abstract class io/sentry/metrics/Metric { public fun getTimeStampMs ()Ljava/lang/Long; public fun getType ()Lio/sentry/metrics/MetricType; public fun getUnit ()Lio/sentry/MeasurementUnit; + public abstract fun getValues ()Ljava/lang/Iterable; public abstract fun getWeight ()I - public abstract fun serialize ()Ljava/lang/Iterable; } public final class io/sentry/metrics/MetricResourceIdentifier { @@ -3494,7 +3494,6 @@ public abstract interface class io/sentry/metrics/MetricsApi$IMetricsInterface { public final class io/sentry/metrics/MetricsHelper { public static final field FLUSHER_SLEEP_TIME_MS I - public static final field MAX_TOTAL_WEIGHT I public fun ()V public static fun convertNanosTo (Lio/sentry/MeasurementUnit$Duration;J)D public static fun encodeMetrics (JLjava/util/Collection;Ljava/lang/StringBuilder;)V @@ -3544,8 +3543,8 @@ public final class io/sentry/metrics/SentryMetric$JsonKeys { public final class io/sentry/metrics/SetMetric : io/sentry/metrics/Metric { public fun (Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun add (D)V + public fun getValues ()Ljava/lang/Iterable; public fun getWeight ()I - public fun serialize ()Ljava/lang/Iterable; } public final class io/sentry/profilemeasurements/ProfileMeasurement : io/sentry/JsonSerializable, io/sentry/JsonUnknown { diff --git a/sentry/src/main/java/io/sentry/MetricsAggregator.java b/sentry/src/main/java/io/sentry/MetricsAggregator.java index de1224b1ba..3f8370ce80 100644 --- a/sentry/src/main/java/io/sentry/MetricsAggregator.java +++ b/sentry/src/main/java/io/sentry/MetricsAggregator.java @@ -18,7 +18,6 @@ import java.util.Set; import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; import java.util.zip.CRC32; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -45,9 +44,6 @@ public final class MetricsAggregator implements IMetricsAggregator, Runnable, Cl // the metrics, // each of which has a key that uniquely identifies it within the time period private final NavigableMap> buckets = new ConcurrentSkipListMap<>(); - private final AtomicInteger totalBucketsWeight = new AtomicInteger(); - - private final int maxWeight; public MetricsAggregator( final @NotNull SentryOptions options, final @NotNull IMetricsClient client) { @@ -55,7 +51,6 @@ public MetricsAggregator( client, options.getLogger(), options.getDateProvider(), - MetricsHelper.MAX_TOTAL_WEIGHT, NoOpSentryExecutorService.getInstance()); } @@ -64,12 +59,10 @@ public MetricsAggregator( final @NotNull IMetricsClient client, final @NotNull ILogger logger, final @NotNull SentryDateProvider dateProvider, - final int maxWeight, final @NotNull ISentryExecutorService executorService) { this.client = client; this.logger = logger; this.dateProvider = dateProvider; - this.maxWeight = maxWeight; this.executorService = executorService; } @@ -160,49 +153,49 @@ private void add( final double value, @Nullable MeasurementUnit unit, final @Nullable Map tags, - @NotNull Long timestampMs, + @Nullable Long timestampMs, final int stackLevel) { if (isClosed) { return; } + if (timestampMs == null) { + timestampMs = nowMillis(); + } + + final @NotNull Metric metric; + switch (type) { + case Counter: + metric = new CounterMetric(key, value, unit, tags, timestampMs); + break; + case Gauge: + metric = new GaugeMetric(key, value, unit, tags, timestampMs); + break; + case Distribution: + metric = new DistributionMetric(key, value, unit, tags, timestampMs); + break; + case Set: + metric = new SetMetric(key, unit, tags, timestampMs); + //noinspection unchecked + metric.add((int) value); + break; + default: + throw new IllegalArgumentException("Unknown MetricType: " + type.name()); + } + final long timeBucketKey = MetricsHelper.getTimeBucketKey(timestampMs); final @NotNull Map timeBucket = getOrAddTimeBucket(timeBucketKey); final @NotNull String metricKey = MetricsHelper.getMetricBucketKey(type, key, unit, tags); - // TODO ideally we can synchronize only the metric itself + // TODO check if we can synchronize only the metric itself synchronized (timeBucket) { @Nullable Metric existingMetric = timeBucket.get(metricKey); if (existingMetric != null) { - final int oldWeight = existingMetric.getWeight(); existingMetric.add(value); - final int newWeight = existingMetric.getWeight(); - totalBucketsWeight.addAndGet(newWeight - oldWeight); } else { - final @NotNull Metric metric; - switch (type) { - case Counter: - metric = new CounterMetric(key, value, unit, tags, timestampMs); - break; - case Gauge: - metric = new GaugeMetric(key, value, unit, tags, timestampMs); - break; - case Distribution: - metric = new DistributionMetric(key, value, unit, tags, timestampMs); - break; - case Set: - metric = new SetMetric(key, unit, tags, timestampMs); - // sets API is either ints or strings cr32 encoded into ints - // noinspection unchecked - metric.add((int) value); - break; - default: - throw new IllegalArgumentException("Unknown MetricType: " + type.name()); - } timeBucket.put(metricKey, metric); - totalBucketsWeight.addAndGet(metric.getWeight()); } } @@ -224,13 +217,7 @@ private void add( } @Override - public void flush(boolean force) { - final int totalWeight = buckets.size() + totalBucketsWeight.get(); - if (totalWeight >= maxWeight) { - logger.log(SentryLevel.INFO, "Metrics: total weight exceeded, flushing all buckets"); - force = true; - } - + public void flush(final boolean force) { final @NotNull Set flushableBuckets = getFlushableBuckets(force); if (flushableBuckets.isEmpty()) { logger.log(SentryLevel.DEBUG, "Metrics: nothing to flush"); @@ -239,21 +226,16 @@ public void flush(boolean force) { logger.log(SentryLevel.DEBUG, "Metrics: flushing " + flushableBuckets.size() + " buckets"); final Map> snapshot = new HashMap<>(); - int numMetrics = 0; + int totalSize = 0; for (long bucketKey : flushableBuckets) { - final @Nullable Map bucket = buckets.remove(bucketKey); - if (bucket != null) { - synchronized (bucket) { - final int weight = getBucketWeight(bucket); - totalBucketsWeight.addAndGet(-weight); - - numMetrics += bucket.size(); - snapshot.put(bucketKey, bucket); - } + final @Nullable Map metrics = buckets.remove(bucketKey); + if (metrics != null) { + totalSize += metrics.size(); + snapshot.put(bucketKey, metrics); } } - if (numMetrics == 0) { + if (totalSize == 0) { logger.log(SentryLevel.DEBUG, "Metrics: only empty buckets found"); return; } @@ -262,14 +244,6 @@ public void flush(boolean force) { client.captureMetrics(new EncodedMetrics(snapshot)); } - private static int getBucketWeight(final @NotNull Map bucket) { - int weight = 0; - for (final @NotNull Metric value : bucket.values()) { - weight += value.getWeight(); - } - return weight; - } - @NotNull private Set getFlushableBuckets(final boolean force) { if (force) { @@ -288,7 +262,7 @@ private Map getOrAddTimeBucket(final long bucketKey) { @Nullable Map bucket = buckets.get(bucketKey); if (bucket == null) { // although buckets is thread safe, we still need to synchronize here to avoid creating - // the same bucket at the same time, overwriting each other + // the same bucket at the same time synchronized (buckets) { bucket = buckets.get(bucketKey); if (bucket == null) { diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java index 62f9c10f23..aeaadc4428 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java @@ -17,7 +17,6 @@ @ApiStatus.Internal public final class MetricsHelper { public static final int FLUSHER_SLEEP_TIME_MS = 5000; - public static final int MAX_TOTAL_WEIGHT = 100000; private static final int ROLLUP_IN_SECONDS = 10; private static final Pattern INVALID_KEY_CHARACTERS_PATTERN = diff --git a/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt index b1180044a4..641ae14f8f 100644 --- a/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt +++ b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt @@ -27,12 +27,11 @@ class MetricsAggregatorTest { var currentTimeMillis: Long = 0 var executorService = DeferredExecutorService() - fun getSut(maxWeight: Int = MetricsHelper.MAX_TOTAL_WEIGHT): MetricsAggregator { + fun getSut(): MetricsAggregator { return MetricsAggregator( client, logger, dateProvider, - maxWeight, executorService ) } @@ -309,38 +308,4 @@ class MetricsAggregatorTest { // and flushing is scheduled again assertTrue(fixture.executorService.hasScheduledRunnables()) } - - @Test - fun `weight is considered for force flushing`() { - // weight is determined by number of buckets + weight of metrics - val aggregator = fixture.getSut(5) - - // when 3 values are emitted - for (i in 0 until 3) { - aggregator.distribution( - "name", - i.toDouble(), - null, - null, - fixture.currentTimeMillis, - 1 - ) - } - // no metrics are captured by the client - aggregator.flush(false) - verify(fixture.client, never()).captureMetrics(any()) - - // once we have 4 values and one bucket = weight of 5 - aggregator.distribution( - "name", - 10.0, - null, - null, - fixture.currentTimeMillis, - 1 - ) - // then flush without force still captures all metrics - aggregator.flush(false) - verify(fixture.client).captureMetrics(any()) - } } From 3da6982aea636420ef0fcbd54439b6310e3d4292 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Thu, 22 Feb 2024 15:18:13 +0100 Subject: [PATCH 19/26] Fix .api --- sentry/api/sentry.api | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 99b32e8c7f..26f107ceba 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3395,15 +3395,15 @@ public final class io/sentry/metrics/CounterMetric : io/sentry/metrics/Metric { public fun (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun add (D)V public fun getValue ()D - public fun getValues ()Ljava/lang/Iterable; public fun getWeight ()I + public fun serialize ()Ljava/lang/Iterable; } public final class io/sentry/metrics/DistributionMetric : io/sentry/metrics/Metric { public fun (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun add (D)V - public fun getValues ()Ljava/lang/Iterable; public fun getWeight ()I + public fun serialize ()Ljava/lang/Iterable; } public final class io/sentry/metrics/EncodedMetrics { @@ -3414,8 +3414,8 @@ public final class io/sentry/metrics/EncodedMetrics { public final class io/sentry/metrics/GaugeMetric : io/sentry/metrics/Metric { public fun (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun add (D)V - public fun getValues ()Ljava/lang/Iterable; public fun getWeight ()I + public fun serialize ()Ljava/lang/Iterable; } public abstract interface class io/sentry/metrics/IMetricsClient { @@ -3430,8 +3430,8 @@ public abstract class io/sentry/metrics/Metric { public fun getTimeStampMs ()Ljava/lang/Long; public fun getType ()Lio/sentry/metrics/MetricType; public fun getUnit ()Lio/sentry/MeasurementUnit; - public abstract fun getValues ()Ljava/lang/Iterable; public abstract fun getWeight ()I + public abstract fun serialize ()Ljava/lang/Iterable; } public final class io/sentry/metrics/MetricResourceIdentifier { @@ -3543,8 +3543,8 @@ public final class io/sentry/metrics/SentryMetric$JsonKeys { public final class io/sentry/metrics/SetMetric : io/sentry/metrics/Metric { public fun (Ljava/lang/String;Lio/sentry/MeasurementUnit;Ljava/util/Map;Ljava/lang/Long;)V public fun add (D)V - public fun getValues ()Ljava/lang/Iterable; public fun getWeight ()I + public fun serialize ()Ljava/lang/Iterable; } public final class io/sentry/profilemeasurements/ProfileMeasurement : io/sentry/JsonSerializable, io/sentry/JsonUnknown { From 576450e5cfbd659fa9500d3dd4fd509bbe53f447 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Mon, 26 Feb 2024 10:12:32 +0100 Subject: [PATCH 20/26] Fix tests --- .../io/sentry/android/core/SessionTrackingIntegrationTest.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt index 3bb853d271..1a441cd832 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt @@ -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 @@ -174,5 +175,9 @@ class SessionTrackingIntegrationTest { override fun getRateLimiter(): RateLimiter? { TODO("Not yet implemented") } + + override fun getMetricsAggregator(): IMetricsAggregator { + TODO("Not yet implemented") + } } } From 43305ed02fb41feeb33c3218c17813986b364831 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Mon, 26 Feb 2024 10:51:28 +0100 Subject: [PATCH 21/26] Fix remove test code --- sentry/src/main/java/io/sentry/Hub.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index 7a8576b9ba..ddce3eac75 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -61,8 +61,6 @@ private Hub(final @NotNull SentryOptions options, final @NotNull Stack stack) { private Hub(final @NotNull SentryOptions options, final @NotNull StackItem rootStackItem) { this(options, new Stack(options.getLogger(), rootStackItem)); - - Sentry.metrics().increment("hub.init"); } private static void validateOptions(final @NotNull SentryOptions options) { From 4fddc57ca19bf26e822dab53267c521bbc4ed052 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 28 Feb 2024 07:04:19 +0100 Subject: [PATCH 22/26] Replace tag values with empty string --- sentry/src/main/java/io/sentry/metrics/MetricsHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java index aeaadc4428..3a3dfff537 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java @@ -63,7 +63,7 @@ public static long getCutoffTimestampMs(final long nowMs) { } public static String sanitizeValue(final @NotNull String input) { - return INVALID_VALUE_CHARACTERS_PATTERN.matcher(input).replaceAll("_"); + return INVALID_VALUE_CHARACTERS_PATTERN.matcher(input).replaceAll(""); } public static @NotNull String toStatsdType(final @NotNull MetricType type) { From fcff26803052621cfcd8c25a46868d2c844139b4 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 28 Feb 2024 07:13:42 +0100 Subject: [PATCH 23/26] Address PR feedback --- sentry/src/main/java/io/sentry/SentryClient.java | 8 ++++---- sentry/src/main/java/io/sentry/SentryEnvelopeItem.java | 2 +- .../main/java/io/sentry/metrics/EncodedMetrics.java | 10 +++++++++- .../src/main/java/io/sentry/metrics/MetricsHelper.java | 5 +++-- .../src/test/java/io/sentry/MetricsAggregatorTest.kt | 6 +++--- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index cae8f447b7..ea70371722 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -992,13 +992,13 @@ private boolean sample() { } @Override - public @NotNull SentryId captureMetrics(@NotNull EncodedMetrics metrics) { + public @NotNull SentryId captureMetrics(final @NotNull EncodedMetrics metrics) { - final SentryEnvelopeItem envelopeItem = SentryEnvelopeItem.fromMetrics(metrics); - final SentryEnvelopeHeader envelopeHeader = + final @NotNull SentryEnvelopeItem envelopeItem = SentryEnvelopeItem.fromMetrics(metrics); + final @NotNull SentryEnvelopeHeader envelopeHeader = new SentryEnvelopeHeader(new SentryId(), options.getSdkVersion(), null); - final SentryEnvelope envelope = + final @NotNull SentryEnvelope envelope = new SentryEnvelope(envelopeHeader, Collections.singleton(envelopeItem)); final @Nullable SentryId id = captureEnvelope(envelope); return id != null ? id : SentryId.EMPTY_ID; diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index e1898cdb6e..45efecfc50 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -199,7 +199,7 @@ public static SentryEnvelopeItem fromMetrics(final @NotNull EncodedMetrics metri () -> { // avoid method refs on Android due to some issues with older AGP setups //noinspection Convert2MethodRef - return metrics.encode(); + return metrics.encodeToStatsd(); }); final @NotNull SentryEnvelopeItemHeader itemHeader = diff --git a/sentry/src/main/java/io/sentry/metrics/EncodedMetrics.java b/sentry/src/main/java/io/sentry/metrics/EncodedMetrics.java index eac1a5465d..255945d851 100644 --- a/sentry/src/main/java/io/sentry/metrics/EncodedMetrics.java +++ b/sentry/src/main/java/io/sentry/metrics/EncodedMetrics.java @@ -19,7 +19,15 @@ public EncodedMetrics(final @NotNull Map> buckets) { this.buckets = buckets; } - public byte[] encode() { + /** + * Encodes the metrics into a Statsd compatible format. + *

See github.com/statsd/statsd#usage + * and getsentry.github.io/relay/relay_metrics/index.html for + * more details about the format. + * + * @return the encoded metrics + */ + public byte[] encodeToStatsd() { final StringBuilder statsd = new StringBuilder(); for (Map.Entry> entry : buckets.entrySet()) { MetricsHelper.encodeMetrics(entry.getKey(), entry.getValue().values(), statsd); diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java index 3a3dfff537..c038a96ecd 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java @@ -164,8 +164,9 @@ public static double convertNanosTo( /** * Encodes the metrics * - *

See github.com/statsd/statsd#usage for - * more details about the format + *

See github.com/statsd/statsd#usage + * and getsentry.github.io/relay/relay_metrics/index.html for + * more details about the format. * * @param timestamp The bucket time the metrics belong to, in second resolution * @param metrics The metrics to encode diff --git a/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt index 641ae14f8f..df82b24ae0 100644 --- a/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt +++ b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt @@ -116,7 +116,7 @@ class MetricsAggregatorTest { verify(fixture.client).captureMetrics( check { - val metrics = MetricsHelperTest.parseMetrics(it.encode()) + val metrics = MetricsHelperTest.parseMetrics(it.encodeToStatsd()) assertEquals(1, metrics.size) assertEquals( MetricsHelperTest.Companion.StatsDMetric( @@ -185,7 +185,7 @@ class MetricsAggregatorTest { // then all of them are emitted separately verify(fixture.client).captureMetrics( check { - val metrics = MetricsHelperTest.parseMetrics(it.encode()) + val metrics = MetricsHelperTest.parseMetrics(it.encodeToStatsd()) assertEquals(5, metrics.size) } ) @@ -272,7 +272,7 @@ class MetricsAggregatorTest { aggregator.flush(true) verify(fixture.client).captureMetrics( check { - val metrics = MetricsHelperTest.parseMetrics(it.encode()) + val metrics = MetricsHelperTest.parseMetrics(it.encodeToStatsd()) assertEquals(6, metrics.size) } ) From 86b3ffa0b15c8e72194fcdb12fcd797486cf8330 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 28 Feb 2024 07:45:55 +0100 Subject: [PATCH 24/26] Format & API --- sentry/api/sentry.api | 2 +- .../src/main/java/io/sentry/metrics/EncodedMetrics.java | 8 +++++--- sentry/src/main/java/io/sentry/metrics/MetricsHelper.java | 7 ++++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index bc9abc736e..a3a3351649 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3410,7 +3410,7 @@ public final class io/sentry/metrics/DistributionMetric : io/sentry/metrics/Metr public final class io/sentry/metrics/EncodedMetrics { public fun (Ljava/util/Map;)V - public fun encode ()[B + public fun encodeToStatsd ()[B } public final class io/sentry/metrics/GaugeMetric : io/sentry/metrics/Metric { diff --git a/sentry/src/main/java/io/sentry/metrics/EncodedMetrics.java b/sentry/src/main/java/io/sentry/metrics/EncodedMetrics.java index 255945d851..2fed55f17a 100644 --- a/sentry/src/main/java/io/sentry/metrics/EncodedMetrics.java +++ b/sentry/src/main/java/io/sentry/metrics/EncodedMetrics.java @@ -21,9 +21,11 @@ public EncodedMetrics(final @NotNull Map> buckets) { /** * Encodes the metrics into a Statsd compatible format. - *

See github.com/statsd/statsd#usage - * and getsentry.github.io/relay/relay_metrics/index.html for - * more details about the format. + * + *

See github.com/statsd/statsd#usage and + * getsentry.github.io/relay/relay_metrics/index.html + * for more details about the format. * * @return the encoded metrics */ diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java index c038a96ecd..47eea12e1d 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java @@ -164,9 +164,10 @@ public static double convertNanosTo( /** * Encodes the metrics * - *

See github.com/statsd/statsd#usage - * and getsentry.github.io/relay/relay_metrics/index.html for - * more details about the format. + *

See github.com/statsd/statsd#usage and + * getsentry.github.io/relay/relay_metrics/index.html + * for more details about the format. * * @param timestamp The bucket time the metrics belong to, in second resolution * @param metrics The metrics to encode From 1788aafb570783e6365567a7aaa881901e7c2273 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 28 Feb 2024 10:51:10 +0100 Subject: [PATCH 25/26] Fix tests --- sentry/src/test/java/io/sentry/metrics/MetricsHelperTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry/src/test/java/io/sentry/metrics/MetricsHelperTest.kt b/sentry/src/test/java/io/sentry/metrics/MetricsHelperTest.kt index b91c572c85..fb1817f235 100644 --- a/sentry/src/test/java/io/sentry/metrics/MetricsHelperTest.kt +++ b/sentry/src/test/java/io/sentry/metrics/MetricsHelperTest.kt @@ -67,9 +67,9 @@ class MetricsHelperTest { @Test fun sanitizeValue() { - assertEquals("_\$foo", MetricsHelper.sanitizeValue("%\$foo")) + assertEquals("\$foo", MetricsHelper.sanitizeValue("%\$foo")) assertEquals("blah{}", MetricsHelper.sanitizeValue("blah{}")) - assertEquals("sn_wm_n", MetricsHelper.sanitizeValue("snöwmän")) + assertEquals("snwmn", MetricsHelper.sanitizeValue("snöwmän")) } @Test From 9b1f4aaf4abdcd29607aaf43a7a2fedaad513c6a Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 28 Feb 2024 11:40:26 +0100 Subject: [PATCH 26/26] 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 07fc4e5ba65233700ee8cc140d9d575b10694f96. * Fix changelog * Address PR feedback * Fix tests * Fix remove test code * Address PR feedback * Update Changelog --- sentry/api/sentry.api | 5 +- .../java/io/sentry/MetricsAggregator.java | 113 +++++++++++------- .../java/io/sentry/metrics/MetricsHelper.java | 3 +- .../java/io/sentry/MetricsAggregatorTest.kt | 78 +++++++++++- 4 files changed, 155 insertions(+), 44 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index a3a3351649..6f79266d2d 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -1023,7 +1023,7 @@ public final class io/sentry/MemoryCollectionData { public final class io/sentry/MetricsAggregator : io/sentry/IMetricsAggregator, java/io/Closeable, java/lang/Runnable { public fun (Lio/sentry/SentryOptions;Lio/sentry/metrics/IMetricsClient;)V - public fun (Lio/sentry/metrics/IMetricsClient;Lio/sentry/ILogger;Lio/sentry/SentryDateProvider;Lio/sentry/ISentryExecutorService;)V + public fun (Lio/sentry/metrics/IMetricsClient;Lio/sentry/ILogger;Lio/sentry/SentryDateProvider;ILio/sentry/ISentryExecutorService;)V public fun close ()V public fun distribution (Ljava/lang/String;DLio/sentry/MeasurementUnit;Ljava/util/Map;JI)V public fun flush (Z)V @@ -3495,7 +3495,8 @@ public abstract interface class io/sentry/metrics/MetricsApi$IMetricsInterface { } public final class io/sentry/metrics/MetricsHelper { - public static final field FLUSHER_SLEEP_TIME_MS I + public static final field FLUSHER_SLEEP_TIME_MS J + public static final field MAX_TOTAL_WEIGHT I public fun ()V public static fun convertNanosTo (Lio/sentry/MeasurementUnit$Duration;J)D public static fun encodeMetrics (JLjava/util/Collection;Ljava/lang/StringBuilder;)V diff --git a/sentry/src/main/java/io/sentry/MetricsAggregator.java b/sentry/src/main/java/io/sentry/MetricsAggregator.java index 3f8370ce80..b7ef77e7f5 100644 --- a/sentry/src/main/java/io/sentry/MetricsAggregator.java +++ b/sentry/src/main/java/io/sentry/MetricsAggregator.java @@ -18,6 +18,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.zip.CRC32; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -43,7 +44,11 @@ public final class MetricsAggregator implements IMetricsAggregator, Runnable, Cl // aggregates all of the metrics data for a particular time period. The Value is a dictionary for // the metrics, // each of which has a key that uniquely identifies it within the time period - private final NavigableMap> buckets = new ConcurrentSkipListMap<>(); + private final @NotNull NavigableMap> buckets = + new ConcurrentSkipListMap<>(); + private final @NotNull AtomicInteger totalBucketsWeight = new AtomicInteger(); + + private final int maxWeight; public MetricsAggregator( final @NotNull SentryOptions options, final @NotNull IMetricsClient client) { @@ -51,6 +56,7 @@ public MetricsAggregator( client, options.getLogger(), options.getDateProvider(), + MetricsHelper.MAX_TOTAL_WEIGHT, NoOpSentryExecutorService.getInstance()); } @@ -59,10 +65,12 @@ public MetricsAggregator( final @NotNull IMetricsClient client, final @NotNull ILogger logger, final @NotNull SentryDateProvider dateProvider, + final int maxWeight, final @NotNull ISentryExecutorService executorService) { this.client = client; this.logger = logger; this.dateProvider = dateProvider; + this.maxWeight = maxWeight; this.executorService = executorService; } @@ -153,71 +161,78 @@ private void add( final double value, @Nullable MeasurementUnit unit, final @Nullable Map tags, - @Nullable Long timestampMs, + @NotNull Long timestampMs, final int stackLevel) { if (isClosed) { return; } - if (timestampMs == null) { - timestampMs = nowMillis(); - } - - final @NotNull Metric metric; - switch (type) { - case Counter: - metric = new CounterMetric(key, value, unit, tags, timestampMs); - break; - case Gauge: - metric = new GaugeMetric(key, value, unit, tags, timestampMs); - break; - case Distribution: - metric = new DistributionMetric(key, value, unit, tags, timestampMs); - break; - case Set: - metric = new SetMetric(key, unit, tags, timestampMs); - //noinspection unchecked - metric.add((int) value); - break; - default: - throw new IllegalArgumentException("Unknown MetricType: " + type.name()); - } - final long timeBucketKey = MetricsHelper.getTimeBucketKey(timestampMs); final @NotNull Map timeBucket = getOrAddTimeBucket(timeBucketKey); final @NotNull String metricKey = MetricsHelper.getMetricBucketKey(type, key, unit, tags); - // TODO check if we can synchronize only the metric itself + // TODO ideally we can synchronize only the metric itself synchronized (timeBucket) { @Nullable Metric existingMetric = timeBucket.get(metricKey); if (existingMetric != null) { + final int oldWeight = existingMetric.getWeight(); existingMetric.add(value); + final int newWeight = existingMetric.getWeight(); + totalBucketsWeight.addAndGet(newWeight - oldWeight); } else { + final @NotNull Metric metric; + switch (type) { + case Counter: + metric = new CounterMetric(key, value, unit, tags, timestampMs); + break; + case Gauge: + metric = new GaugeMetric(key, value, unit, tags, timestampMs); + break; + case Distribution: + metric = new DistributionMetric(key, value, unit, tags, timestampMs); + break; + case Set: + metric = new SetMetric(key, unit, tags, timestampMs); + // sets API is either ints or strings cr32 encoded into ints + // noinspection unchecked + metric.add((int) value); + break; + default: + throw new IllegalArgumentException("Unknown MetricType: " + type.name()); + } timeBucket.put(metricKey, metric); + totalBucketsWeight.addAndGet(metric.getWeight()); } } - // spin up real executor service the first time metrics are collected - if (!isClosed && !flushScheduled) { + final boolean isOverWeight = isOverWeight(); + if (!isClosed && (isOverWeight || !flushScheduled)) { synchronized (this) { - if (!isClosed && !flushScheduled) { - flushScheduled = true; + if (!isClosed) { // TODO this is probably not a good idea after all // as it will slow down the first metric emission // maybe move to constructor? if (executorService instanceof NoOpSentryExecutorService) { executorService = new SentryExecutorService(); } - executorService.schedule(this, MetricsHelper.FLUSHER_SLEEP_TIME_MS); + + flushScheduled = true; + final long delayMs = isOverWeight ? 0 : MetricsHelper.FLUSHER_SLEEP_TIME_MS; + executorService.schedule(this, delayMs); } } } } @Override - public void flush(final boolean force) { + public void flush(boolean force) { + if (!force && isOverWeight()) { + logger.log(SentryLevel.INFO, "Metrics: total weight exceeded, flushing all buckets"); + force = true; + } + final @NotNull Set flushableBuckets = getFlushableBuckets(force); if (flushableBuckets.isEmpty()) { logger.log(SentryLevel.DEBUG, "Metrics: nothing to flush"); @@ -226,16 +241,21 @@ public void flush(final boolean force) { logger.log(SentryLevel.DEBUG, "Metrics: flushing " + flushableBuckets.size() + " buckets"); final Map> snapshot = new HashMap<>(); - int totalSize = 0; + int numMetrics = 0; for (long bucketKey : flushableBuckets) { - final @Nullable Map metrics = buckets.remove(bucketKey); - if (metrics != null) { - totalSize += metrics.size(); - snapshot.put(bucketKey, metrics); + final @Nullable Map bucket = buckets.remove(bucketKey); + if (bucket != null) { + synchronized (bucket) { + final int weight = getBucketWeight(bucket); + totalBucketsWeight.addAndGet(-weight); + + numMetrics += bucket.size(); + snapshot.put(bucketKey, bucket); + } } } - if (totalSize == 0) { + if (numMetrics == 0) { logger.log(SentryLevel.DEBUG, "Metrics: only empty buckets found"); return; } @@ -244,6 +264,19 @@ public void flush(final boolean force) { client.captureMetrics(new EncodedMetrics(snapshot)); } + private boolean isOverWeight() { + final int totalWeight = buckets.size() + totalBucketsWeight.get(); + return totalWeight >= maxWeight; + } + + private static int getBucketWeight(final @NotNull Map bucket) { + int weight = 0; + for (final @NotNull Metric value : bucket.values()) { + weight += value.getWeight(); + } + return weight; + } + @NotNull private Set getFlushableBuckets(final boolean force) { if (force) { @@ -262,7 +295,7 @@ private Map getOrAddTimeBucket(final long bucketKey) { @Nullable Map bucket = buckets.get(bucketKey); if (bucket == null) { // although buckets is thread safe, we still need to synchronize here to avoid creating - // the same bucket at the same time + // the same bucket at the same time, overwriting each other synchronized (buckets) { bucket = buckets.get(bucketKey); if (bucket == null) { diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java index 47eea12e1d..7b5714230d 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsHelper.java @@ -16,7 +16,8 @@ @ApiStatus.Internal public final class MetricsHelper { - public static final int FLUSHER_SLEEP_TIME_MS = 5000; + public static final long FLUSHER_SLEEP_TIME_MS = 5000; + public static final int MAX_TOTAL_WEIGHT = 100000; private static final int ROLLUP_IN_SECONDS = 10; private static final Pattern INVALID_KEY_CHARACTERS_PATTERN = diff --git a/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt index df82b24ae0..5e3b575594 100644 --- a/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt +++ b/sentry/src/test/java/io/sentry/MetricsAggregatorTest.kt @@ -6,6 +6,7 @@ import io.sentry.metrics.MetricsHelperTest import io.sentry.test.DeferredExecutorService import org.mockito.kotlin.any import org.mockito.kotlin.check +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify @@ -27,11 +28,12 @@ class MetricsAggregatorTest { var currentTimeMillis: Long = 0 var executorService = DeferredExecutorService() - fun getSut(): MetricsAggregator { + fun getSut(maxWeight: Int = MetricsHelper.MAX_TOTAL_WEIGHT): MetricsAggregator { return MetricsAggregator( client, logger, dateProvider, + maxWeight, executorService ) } @@ -308,4 +310,78 @@ class MetricsAggregatorTest { // and flushing is scheduled again assertTrue(fixture.executorService.hasScheduledRunnables()) } + + @Test + fun `weight is considered for force flushing`() { + // weight is determined by number of buckets + weight of metrics + val aggregator = fixture.getSut(5) + + // when 3 values are emitted + for (i in 0 until 3) { + aggregator.distribution( + "name", + i.toDouble(), + null, + null, + fixture.currentTimeMillis, + 1 + ) + } + // no metrics are captured by the client + fixture.executorService.runAll() + verify(fixture.client, never()).captureMetrics(any()) + + // once we have 4 values and one bucket = weight of 5 + aggregator.distribution( + "name", + 10.0, + null, + null, + fixture.currentTimeMillis, + 1 + ) + // then flush without force still captures all metrics + fixture.executorService.runAll() + verify(fixture.client).captureMetrics(any()) + } + + @Test + fun `flushing is immediately scheduled if add operations causes too much weight`() { + fixture.executorService = mock() + val aggregator = fixture.getSut(1) + + verify(fixture.executorService, never()).schedule(any(), any()) + + // when 1 value is emitted + aggregator.distribution( + "name", + 1.0, + null, + null, + fixture.currentTimeMillis, + 1 + ) + + // flush is immediately scheduled + verify(fixture.executorService).schedule(any(), eq(0)) + } + + @Test + fun `flushing is deferred scheduled if add operations does not cause too much weight`() { + fixture.executorService = mock() + val aggregator = fixture.getSut(10) + + // when 1 value is emitted + aggregator.distribution( + "name", + 1.0, + null, + null, + fixture.currentTimeMillis, + 1 + ) + + // flush is scheduled for later + verify(fixture.executorService).schedule(any(), eq(MetricsHelper.FLUSHER_SLEEP_TIME_MS)) + } }