From 5794a6c752cb239b099540b4bbc18b89c6e03f94 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 5 Dec 2023 12:09:57 +1300 Subject: [PATCH 01/52] Added basic metric types --- src/Sentry/Protocol/Metric.cs | 86 +++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/Sentry/Protocol/Metric.cs diff --git a/src/Sentry/Protocol/Metric.cs b/src/Sentry/Protocol/Metric.cs new file mode 100644 index 0000000000..b8e09f2089 --- /dev/null +++ b/src/Sentry/Protocol/Metric.cs @@ -0,0 +1,86 @@ +using System.Runtime.InteropServices.Marshalling; +using Sentry.Extensibility; +using Sentry.Internal.Extensions; + +namespace Sentry.Protocol; + +internal abstract class Metric : IJsonSerializable +{ + public string Name { get; set; } + public DateTime Timestamp { get; set; } + public MeasurementUnit? Unit { get; set; } + + private readonly Lazy> _tags = new(() => new Dictionary()); + public IDictionary Tags => _tags.Value; + + public abstract void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger); + + public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) + { + writer.WriteStartObject(); + writer.WriteString("name", Name.ToString()); + writer.WriteString("timestamp", Timestamp); + if (Unit.HasValue) + { + writer.WriteStringIfNotWhiteSpace("unit", Unit.ToString()); + } + writer.WriteStringDictionaryIfNotEmpty("tags", Tags!); + WriteConcreteProperties(writer, logger); + writer.WriteEndObject(); + } +} + +/// +/// Counters track a value that can only be incremented. +/// +internal class CounterMetric : Metric { + private int Value { get; set; } + + public override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) => + writer.WriteNumber("value", Value); +} + +/// +/// Gauges track a value that can go up and down. +/// +internal class GaugeMetric : Metric +{ + double Value { get; set; } + double First { get; set; } + double Min { get; set; } + double Max { get; set; } + double Sum { get; set; } + double Count { get; set; } + + public override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) + { + writer.WriteNumber("value", Value); + writer.WriteNumber("first", First); + writer.WriteNumber("min", Min); + writer.WriteNumber("max", Max); + writer.WriteNumber("sum", Sum); + writer.WriteNumber("count", Count); + } +} + +/// +/// Distributions track a list of values over time in on which you can perform aggregations like max, min, avg. +/// +internal class DistributionMetric : Metric +{ + IEnumerable Value { get; set; } = new List(); + + public override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) => + writer.WriteArrayIfNotEmpty("value", Value, logger); +} + +/// +/// Sets track a set of values on which you can perform aggregations such as count_unique. +/// +internal class SetMetric : Metric +{ + private HashSet Value { get; set; } = new(); + + public override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) => + writer.WriteArrayIfNotEmpty("value", Value, logger); +} From 3752fd5940580ca9aa6835f1220bcccbd5bb605f Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 6 Dec 2023 11:07:01 +1300 Subject: [PATCH 02/52] Moved Metric classes --- src/Sentry/Protocol/Metric.cs | 86 ------------------- src/Sentry/Protocol/Metrics/CounterMetric.cs | 14 +++ .../Protocol/Metrics/DistributionMetric.cs | 15 ++++ src/Sentry/Protocol/Metrics/GaugeMetric.cs | 26 ++++++ src/Sentry/Protocol/Metrics/Metric.cs | 30 +++++++ src/Sentry/Protocol/Metrics/SetMetric.cs | 15 ++++ 6 files changed, 100 insertions(+), 86 deletions(-) delete mode 100644 src/Sentry/Protocol/Metric.cs create mode 100644 src/Sentry/Protocol/Metrics/CounterMetric.cs create mode 100644 src/Sentry/Protocol/Metrics/DistributionMetric.cs create mode 100644 src/Sentry/Protocol/Metrics/GaugeMetric.cs create mode 100644 src/Sentry/Protocol/Metrics/Metric.cs create mode 100644 src/Sentry/Protocol/Metrics/SetMetric.cs diff --git a/src/Sentry/Protocol/Metric.cs b/src/Sentry/Protocol/Metric.cs deleted file mode 100644 index b8e09f2089..0000000000 --- a/src/Sentry/Protocol/Metric.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.Runtime.InteropServices.Marshalling; -using Sentry.Extensibility; -using Sentry.Internal.Extensions; - -namespace Sentry.Protocol; - -internal abstract class Metric : IJsonSerializable -{ - public string Name { get; set; } - public DateTime Timestamp { get; set; } - public MeasurementUnit? Unit { get; set; } - - private readonly Lazy> _tags = new(() => new Dictionary()); - public IDictionary Tags => _tags.Value; - - public abstract void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger); - - public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) - { - writer.WriteStartObject(); - writer.WriteString("name", Name.ToString()); - writer.WriteString("timestamp", Timestamp); - if (Unit.HasValue) - { - writer.WriteStringIfNotWhiteSpace("unit", Unit.ToString()); - } - writer.WriteStringDictionaryIfNotEmpty("tags", Tags!); - WriteConcreteProperties(writer, logger); - writer.WriteEndObject(); - } -} - -/// -/// Counters track a value that can only be incremented. -/// -internal class CounterMetric : Metric { - private int Value { get; set; } - - public override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) => - writer.WriteNumber("value", Value); -} - -/// -/// Gauges track a value that can go up and down. -/// -internal class GaugeMetric : Metric -{ - double Value { get; set; } - double First { get; set; } - double Min { get; set; } - double Max { get; set; } - double Sum { get; set; } - double Count { get; set; } - - public override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) - { - writer.WriteNumber("value", Value); - writer.WriteNumber("first", First); - writer.WriteNumber("min", Min); - writer.WriteNumber("max", Max); - writer.WriteNumber("sum", Sum); - writer.WriteNumber("count", Count); - } -} - -/// -/// Distributions track a list of values over time in on which you can perform aggregations like max, min, avg. -/// -internal class DistributionMetric : Metric -{ - IEnumerable Value { get; set; } = new List(); - - public override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) => - writer.WriteArrayIfNotEmpty("value", Value, logger); -} - -/// -/// Sets track a set of values on which you can perform aggregations such as count_unique. -/// -internal class SetMetric : Metric -{ - private HashSet Value { get; set; } = new(); - - public override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) => - writer.WriteArrayIfNotEmpty("value", Value, logger); -} diff --git a/src/Sentry/Protocol/Metrics/CounterMetric.cs b/src/Sentry/Protocol/Metrics/CounterMetric.cs new file mode 100644 index 0000000000..67626cbb9b --- /dev/null +++ b/src/Sentry/Protocol/Metrics/CounterMetric.cs @@ -0,0 +1,14 @@ +using Sentry.Extensibility; + +namespace Sentry.Protocol.Metrics; + +/// +/// Counters track a value that can only be incremented. +/// +internal class CounterMetric : Metric +{ + public int Value { get; set; } + + public override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) => + writer.WriteNumber("value", Value); +} diff --git a/src/Sentry/Protocol/Metrics/DistributionMetric.cs b/src/Sentry/Protocol/Metrics/DistributionMetric.cs new file mode 100644 index 0000000000..ccf66d3519 --- /dev/null +++ b/src/Sentry/Protocol/Metrics/DistributionMetric.cs @@ -0,0 +1,15 @@ +using Sentry.Extensibility; +using Sentry.Internal.Extensions; + +namespace Sentry.Protocol.Metrics; + +/// +/// Distributions track a list of values over time in on which you can perform aggregations like max, min, avg. +/// +internal class DistributionMetric : Metric +{ + IEnumerable Value { get; set; } = new List(); + + public override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) => + writer.WriteArrayIfNotEmpty("value", Value, logger); +} diff --git a/src/Sentry/Protocol/Metrics/GaugeMetric.cs b/src/Sentry/Protocol/Metrics/GaugeMetric.cs new file mode 100644 index 0000000000..21b3721108 --- /dev/null +++ b/src/Sentry/Protocol/Metrics/GaugeMetric.cs @@ -0,0 +1,26 @@ +using Sentry.Extensibility; + +namespace Sentry.Protocol.Metrics; + +/// +/// Gauges track a value that can go up and down. +/// +internal class GaugeMetric : Metric +{ + double Value { get; set; } + double First { get; set; } + double Min { get; set; } + double Max { get; set; } + double Sum { get; set; } + double Count { get; set; } + + public override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) + { + writer.WriteNumber("value", Value); + writer.WriteNumber("first", First); + writer.WriteNumber("min", Min); + writer.WriteNumber("max", Max); + writer.WriteNumber("sum", Sum); + writer.WriteNumber("count", Count); + } +} diff --git a/src/Sentry/Protocol/Metrics/Metric.cs b/src/Sentry/Protocol/Metrics/Metric.cs new file mode 100644 index 0000000000..b73d55b063 --- /dev/null +++ b/src/Sentry/Protocol/Metrics/Metric.cs @@ -0,0 +1,30 @@ +using Sentry.Extensibility; +using Sentry.Internal.Extensions; + +namespace Sentry.Protocol.Metrics; + +internal abstract class Metric : IJsonSerializable +{ + public string Name { get; set; } + public DateTime Timestamp { get; set; } = DateTime.Now; + public MeasurementUnit? Unit { get; set; } + + private readonly Lazy> _tags = new(() => new Dictionary()); + public IDictionary Tags => _tags.Value; + + public abstract void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger); + + public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) + { + writer.WriteStartObject(); + writer.WriteString("name", Name.ToString()); + writer.WriteString("timestamp", Timestamp); + if (Unit.HasValue) + { + writer.WriteStringIfNotWhiteSpace("unit", Unit.ToString()); + } + writer.WriteStringDictionaryIfNotEmpty("tags", Tags!); + WriteConcreteProperties(writer, logger); + writer.WriteEndObject(); + } +} diff --git a/src/Sentry/Protocol/Metrics/SetMetric.cs b/src/Sentry/Protocol/Metrics/SetMetric.cs new file mode 100644 index 0000000000..e365391b1f --- /dev/null +++ b/src/Sentry/Protocol/Metrics/SetMetric.cs @@ -0,0 +1,15 @@ +using Sentry.Extensibility; +using Sentry.Internal.Extensions; + +namespace Sentry.Protocol.Metrics; + +/// +/// Sets track a set of values on which you can perform aggregations such as count_unique. +/// +internal class SetMetric : Metric +{ + private HashSet Value { get; set; } = new(); + + public override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) => + writer.WriteArrayIfNotEmpty("value", Value, logger); +} From 0d6587b9f677df8aa7a7fbac8ab91e4db3bf0e4c Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 6 Dec 2023 13:30:31 +1300 Subject: [PATCH 03/52] Added Increment aggregator --- src/Sentry/MetricAggregator.cs | 126 ++++++++++++++++++++ src/Sentry/MetricData.cs | 14 +++ src/Sentry/Protocol/Metrics/Metric.cs | 4 +- test/Sentry.Tests/MetricsAggregatorTests.cs | 97 +++++++++++++++ 4 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 src/Sentry/MetricAggregator.cs create mode 100644 src/Sentry/MetricData.cs create mode 100644 test/Sentry.Tests/MetricsAggregatorTests.cs diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs new file mode 100644 index 0000000000..1fcd2987ee --- /dev/null +++ b/src/Sentry/MetricAggregator.cs @@ -0,0 +1,126 @@ +using Sentry.Internal.Extensions; +using Sentry.Protocol.Metrics; + +namespace Sentry; + +internal class MetricAggregator +{ + private const int RollupInSeconds = 10; + private readonly TimeSpan _flushInterval = TimeSpan.FromSeconds(5); + + // 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 + internal ConcurrentDictionary> Buckets => _buckets.Value; + private readonly Lazy>> _buckets + = new(() => new ConcurrentDictionary>()); + + // private readonly Timer _flushTimer; + // private readonly Action> _onFlush; + + // public MetricsAggregator(TimeSpan? flushInterval, Action> onFlush) + // { + // if (flushInterval.HasValue) + // { + // _flushInterval = flushInterval.Value; + // } + // _onFlush = onFlush; + // _flushTimer = new Timer(FlushData, null, _flushInterval, _flushInterval); + // } + + private static readonly DateTime EpochStart = new(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + internal static long GetTimeBucketKey(DateTime timestamp) + { + var seconds = (long)(timestamp.ToUniversalTime() - EpochStart).TotalSeconds; + + // var seconds = (timestamp?.ToUniversalTime() ?? DateTimeOffset.UtcNow).ToUnixTimeSeconds(); + return (seconds / RollupInSeconds) * RollupInSeconds; + } + + internal static string GetMetricBucketKey(MetricType type, string metricKey, MeasurementUnit unit, IDictionary? tags) + { + var typePrefix = type switch + { + MetricType.Counter => "c", + MetricType.Gauge => "g", + MetricType.Distribution => "d", + MetricType.Set => "s", + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; + var serializedTags = tags?.ToUtf8Json() ?? string.Empty; + + return $"{typePrefix}_{metricKey}_{unit}_{serializedTags}"; + } + + /// + /// Emit a counter. + /// + public void Increment( + string key, + double value = 1.0, + MeasurementUnit? unit = null, + IDictionary? tags = null, + DateTime? timestamp = null + // , int stacklevel = 0 // Used for code locations + ) + { + timestamp ??= DateTime.UtcNow; + var timeBucket = Buckets.GetOrAdd( + GetTimeBucketKey(timestamp.Value), + _ => new ConcurrentDictionary() + ); + + timeBucket.AddOrUpdate( + GetMetricBucketKey(MetricType.Counter, key, unit ?? MeasurementUnit.None, tags), + _ => new MetricData + { + Type = MetricType.Counter, + Key = key, + Value = value, + Unit = unit ?? MeasurementUnit.None, + Timestamp = timestamp.Value, + Tags = tags + }, + (_, metric) => + { + metric.Value += value; + return metric; + } + ); + } + + // // Emit a gauge. + // public void Gauge(string gaugeName, double value, IDictionary tags); + // + // // Emit a distribution. + // public void Distribution(string distributionName, double value, IDictionary tags, string? unit = "second"); + // + // // Emit a set + // public void Set(string key, string value, IDictionary tags); + + // private void FlushData(object? state) + // { + // var metricsToFlush = new List(); + // + // foreach (var metric in _metrics) + // { + // metricsToFlush.Add(metric.Value); + // // Optionally, reset or remove the metric from _metrics + // } + // + // _onFlush(metricsToFlush); + // } + // + // // Method to force flush the data + // public void ForceFlush() + // { + // FlushData(null); + // } + // + // // Dispose pattern to clean up resources + // public void Dispose() + // { + // _flushTimer?.Dispose(); + // } + +} diff --git a/src/Sentry/MetricData.cs b/src/Sentry/MetricData.cs new file mode 100644 index 0000000000..748512a142 --- /dev/null +++ b/src/Sentry/MetricData.cs @@ -0,0 +1,14 @@ +namespace Sentry; + +internal enum MetricType : byte { Counter, Gauge, Distribution, Set } + +internal class MetricData +{ + public MetricType Type { get; set; } + public string Key { get; set; } = string.Empty; // TODO: Replace with constructor + public double Value { get; set; } + public MeasurementUnit Unit { get; set; } + + public IDictionary? Tags { get; set; } + public DateTime Timestamp { get; set; } +} diff --git a/src/Sentry/Protocol/Metrics/Metric.cs b/src/Sentry/Protocol/Metrics/Metric.cs index b73d55b063..0b94112d86 100644 --- a/src/Sentry/Protocol/Metrics/Metric.cs +++ b/src/Sentry/Protocol/Metrics/Metric.cs @@ -5,8 +5,8 @@ namespace Sentry.Protocol.Metrics; internal abstract class Metric : IJsonSerializable { - public string Name { get; set; } - public DateTime Timestamp { get; set; } = DateTime.Now; + public string Name { get; set; } = string.Empty; + public DateTime Timestamp { get; set; } = DateTime.Now; // TODO: Replace with constructor public MeasurementUnit? Unit { get; set; } private readonly Lazy> _tags = new(() => new Dictionary()); diff --git a/test/Sentry.Tests/MetricsAggregatorTests.cs b/test/Sentry.Tests/MetricsAggregatorTests.cs new file mode 100644 index 0000000000..d9408b78a3 --- /dev/null +++ b/test/Sentry.Tests/MetricsAggregatorTests.cs @@ -0,0 +1,97 @@ +namespace Sentry.Tests; + +public class MetricsAggregatorTests +{ + class Fixture + { + public MetricAggregator GetSut() + => new(); + } + + private readonly Fixture _fixture = new(); + + [Theory] + [InlineData(30)] + [InlineData(31)] + [InlineData(39)] + public void GetTimeBucketKey_RoundsDownToNearestTenSeconds(int seconds) + { + // Arrange + // Returns the number of seconds that have elapsed since 1970-01-01T00:00:00Z + // var timestamp = new DateTime(2023, 1, 15, 17, 42, 31, DateTimeKind.Utc); + var timestamp = new DateTime(1970, 1, 1, 1, 1, seconds, DateTimeKind.Utc); + + // Act + var result = MetricAggregator.GetTimeBucketKey(timestamp); + + // Assert + result.Should().Be(3690); // (1 hour) + (1 minute) plus (30 seconds) = 3690 + } + + [Fact] + public void GetMetricBucketKey_GeneratesExpectedKey() + { + // Arrange + var type = MetricType.Counter; + var metricKey = "quibbles"; + var unit = MeasurementUnit.None; + var tags = new Dictionary { ["tag1"] = "value1" }; + + // Act + var result = MetricAggregator.GetMetricBucketKey(type, metricKey, unit, tags); + + // Assert + result.Should().Be("c_quibbles_none_{\"tag1\":\"value1\"}"); + } + + [Fact] + public void Increment_NoMetric_CreatesBucketAndMetric() + { + // Arrange + var key = "counter_key"; + var value = 5.0; + var unit = MeasurementUnit.None; + var tags = new Dictionary { ["tag1"] = "value1" }; + var timestamp = DateTime.UtcNow; + + var sut = new MetricAggregator(); + + // Act + sut.Increment(key, value, unit, tags, timestamp); + + // Assert + var timeBucket = sut.Buckets[MetricAggregator.GetTimeBucketKey(timestamp)]; + var metric = timeBucket[MetricAggregator.GetMetricBucketKey(MetricType.Counter, key, unit, tags)]; + + metric.Value.Should().Be(value); + } + + [Fact] + public void Increment_MultipleMetrics_Aggregates() + { + // Arrange + var key = "counter_key"; + var unit = MeasurementUnit.None; + var tags = new Dictionary { ["tag1"] = "value1" }; + var sut = new MetricAggregator(); + + // Act + DateTime firstTime = new(1970, 1, 1, 0, 0, 31, 0, DateTimeKind.Utc); + sut.Increment(key, 3, unit, tags, firstTime); + + DateTime secondTime = new(1970, 1, 1, 0, 0, 38, 0, DateTimeKind.Utc); + sut.Increment(key, 5, unit, tags, secondTime); + + DateTime thirdTime = new(1970, 1, 1, 0, 0, 40, 0, DateTimeKind.Utc); + sut.Increment(key, 13, unit, tags, thirdTime); + + // Assert + var bucket1 = sut.Buckets[MetricAggregator.GetTimeBucketKey(firstTime)]; + var data1 = bucket1[MetricAggregator.GetMetricBucketKey(MetricType.Counter, key, unit, tags)]; + data1.Value.Should().Be(8); // First two emits are in the same bucket + + var bucket2 = sut.Buckets[MetricAggregator.GetTimeBucketKey(thirdTime)]; + var data2 = bucket2[MetricAggregator.GetMetricBucketKey(MetricType.Counter, key, unit, tags)]; + data2.Value.Should().Be(13); // First two emits are in the same bucket + } +} From 088e4fda4b4cf106a8656e31a4a3f1f465ad7570 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 6 Dec 2023 15:22:42 +1300 Subject: [PATCH 04/52] Implemented Gauge metric --- src/Sentry/MetricAggregator.cs | 114 ++++++++++++++---- src/Sentry/MetricData.cs | 14 --- src/Sentry/Protocol/Metrics/CounterMetric.cs | 20 ++- .../Protocol/Metrics/DistributionMetric.cs | 21 +++- src/Sentry/Protocol/Metrics/GaugeMetric.cs | 44 +++++-- src/Sentry/Protocol/Metrics/Metric.cs | 27 +++-- src/Sentry/Protocol/Metrics/SetMetric.cs | 20 ++- test/Sentry.Tests/MetricsAggregatorTests.cs | 69 +++++++---- 8 files changed, 244 insertions(+), 85 deletions(-) delete mode 100644 src/Sentry/MetricData.cs diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index 1fcd2987ee..4d8b637235 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -8,12 +8,15 @@ internal class MetricAggregator private const int RollupInSeconds = 10; private readonly TimeSpan _flushInterval = TimeSpan.FromSeconds(5); + internal enum MetricType : byte { Counter, Gauge, Distribution, Set } + + // 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 - internal ConcurrentDictionary> Buckets => _buckets.Value; - private readonly Lazy>> _buckets - = new(() => new ConcurrentDictionary>()); + internal ConcurrentDictionary> Buckets => _buckets.Value; + private readonly Lazy>> _buckets + = new(() => new ConcurrentDictionary>()); // private readonly Timer _flushTimer; // private readonly Action> _onFlush; @@ -53,8 +56,13 @@ internal static string GetMetricBucketKey(MetricType type, string metricKey, Mea } /// - /// Emit a counter. + /// Emits a Counter metric /// + /// A unique key identifying the metric + /// The value to be added + /// An optional + /// Optional Tags to associate with the metric + /// The time when the metric was emitted public void Increment( string key, double value = 1.0, @@ -62,42 +70,96 @@ public void Increment( IDictionary? tags = null, DateTime? timestamp = null // , int stacklevel = 0 // Used for code locations - ) + ) => Emit(MetricType.Counter, key, value, unit, tags, timestamp); + + /// + /// Emits a Gauge metric + /// + /// A unique key identifying the metric + /// The value to be added + /// An optional + /// Optional Tags to associate with the metric + /// The time when the metric was emitted + public void Gauge( + string key, + double value = 1.0, + MeasurementUnit? unit = null, + IDictionary? tags = null, + DateTime? timestamp = null + // , int stacklevel = 0 // Used for code locations + ) => Emit(MetricType.Gauge, key, value, unit, tags, timestamp); + + /// + /// Emits a Distribution metric + /// + /// A unique key identifying the metric + /// The value to be added + /// An optional + /// Optional Tags to associate with the metric + /// The time when the metric was emitted + public void Distribution( + string key, + double value = 1.0, + MeasurementUnit? unit = null, + IDictionary? tags = null, + DateTime? timestamp = null + // , int stacklevel = 0 // Used for code locations + ) => Emit(MetricType.Distribution, key, value, unit, tags, timestamp); + + /// + /// Emits a Set metric + /// + /// A unique key identifying the metric + /// The value to be added + /// An optional + /// Optional Tags to associate with the metric + /// The time when the metric was emitted + public void Set( + string key, + double value = 1.0, + MeasurementUnit? unit = null, + IDictionary? tags = null, + DateTime? timestamp = null + // , int stacklevel = 0 // Used for code locations + ) => Emit(MetricType.Set, key, value, unit, tags, timestamp); + + private void Emit( + MetricType type, + string key, + double value = 1.0, + MeasurementUnit? unit = null, + IDictionary? tags = null, + DateTime? timestamp = null + // , int stacklevel = 0 // Used for code locations + ) { timestamp ??= DateTime.UtcNow; + unit ??= MeasurementUnit.None; var timeBucket = Buckets.GetOrAdd( GetTimeBucketKey(timestamp.Value), - _ => new ConcurrentDictionary() + _ => new ConcurrentDictionary() ); + Func addValuesFactory = type switch + { + MetricType.Counter => (string _) => new CounterMetric(key, value, unit.Value, tags), + MetricType.Gauge => (string _) => new GaugeMetric(key, value, unit.Value, tags), + MetricType.Distribution => (string _) => new DistributionMetric(key, value, unit.Value, tags), + MetricType.Set => (string _) => new SetMetric(key, (int)value, unit.Value, tags), + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; + timeBucket.AddOrUpdate( - GetMetricBucketKey(MetricType.Counter, key, unit ?? MeasurementUnit.None, tags), - _ => new MetricData - { - Type = MetricType.Counter, - Key = key, - Value = value, - Unit = unit ?? MeasurementUnit.None, - Timestamp = timestamp.Value, - Tags = tags - }, + GetMetricBucketKey(type, key, unit.Value, tags), + addValuesFactory, (_, metric) => { - metric.Value += value; + metric.Add(value); return metric; } ); } - // // Emit a gauge. - // public void Gauge(string gaugeName, double value, IDictionary tags); - // - // // Emit a distribution. - // public void Distribution(string distributionName, double value, IDictionary tags, string? unit = "second"); - // - // // Emit a set - // public void Set(string key, string value, IDictionary tags); - // private void FlushData(object? state) // { // var metricsToFlush = new List(); diff --git a/src/Sentry/MetricData.cs b/src/Sentry/MetricData.cs deleted file mode 100644 index 748512a142..0000000000 --- a/src/Sentry/MetricData.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Sentry; - -internal enum MetricType : byte { Counter, Gauge, Distribution, Set } - -internal class MetricData -{ - public MetricType Type { get; set; } - public string Key { get; set; } = string.Empty; // TODO: Replace with constructor - public double Value { get; set; } - public MeasurementUnit Unit { get; set; } - - public IDictionary? Tags { get; set; } - public DateTime Timestamp { get; set; } -} diff --git a/src/Sentry/Protocol/Metrics/CounterMetric.cs b/src/Sentry/Protocol/Metrics/CounterMetric.cs index 67626cbb9b..6566d73ea3 100644 --- a/src/Sentry/Protocol/Metrics/CounterMetric.cs +++ b/src/Sentry/Protocol/Metrics/CounterMetric.cs @@ -7,8 +7,24 @@ namespace Sentry.Protocol.Metrics; /// internal class CounterMetric : Metric { - public int Value { get; set; } + public CounterMetric() + { + Value = 0; + } - public override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) => + /// + /// Counters track a value that can only be incremented. + /// + public CounterMetric(string key, double value, MeasurementUnit? unit = null, IDictionary? tags = null) + : base(key, unit, tags) + { + Value = value; + } + + public double Value { get; private set; } + + public override void Add(double value) => Value += value; + + protected override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) => writer.WriteNumber("value", Value); } diff --git a/src/Sentry/Protocol/Metrics/DistributionMetric.cs b/src/Sentry/Protocol/Metrics/DistributionMetric.cs index ccf66d3519..6203a1b7a7 100644 --- a/src/Sentry/Protocol/Metrics/DistributionMetric.cs +++ b/src/Sentry/Protocol/Metrics/DistributionMetric.cs @@ -8,8 +8,25 @@ namespace Sentry.Protocol.Metrics; /// internal class DistributionMetric : Metric { - IEnumerable Value { get; set; } = new List(); + public DistributionMetric() + { + Value = new List(); + } - public override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) => + public DistributionMetric(string key, double value, MeasurementUnit? unit = null, + IDictionary? tags = null) + : base(key, unit, tags) + { + Value = new List() { value }; + } + + public IList Value { get; set; } + + public override void Add(double value) + { + Value.Add(value); + } + + protected override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) => writer.WriteArrayIfNotEmpty("value", Value, logger); } diff --git a/src/Sentry/Protocol/Metrics/GaugeMetric.cs b/src/Sentry/Protocol/Metrics/GaugeMetric.cs index 21b3721108..fd231c152f 100644 --- a/src/Sentry/Protocol/Metrics/GaugeMetric.cs +++ b/src/Sentry/Protocol/Metrics/GaugeMetric.cs @@ -7,14 +7,44 @@ namespace Sentry.Protocol.Metrics; /// internal class GaugeMetric : Metric { - double Value { get; set; } - double First { get; set; } - double Min { get; set; } - double Max { get; set; } - double Sum { get; set; } - double Count { get; set; } + public GaugeMetric() + { + Value = 0; + First = 0; + Min = 0; + Max = 0; + Sum = 0; + Count = 0; + } + + public GaugeMetric(string key, double value, MeasurementUnit? unit = null, IDictionary? tags = null) + : base(key, unit, tags) + { + Value = value; + First = value; + Min = value; + Max = value; + Sum = value; + Count = 1; + } + + public double Value { get; private set; } + public double First { get; private set; } + public double Min { get; private set; } + public double Max { get; private set; } + public double Sum { get; private set; } + public double Count { get; private set; } + + public override void Add(double value) + { + Value = value; + Min = Math.Min(Min, value); + Max = Math.Max(Max, value); + Sum += value; + Count++; + } - public override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) + protected override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) { writer.WriteNumber("value", Value); writer.WriteNumber("first", First); diff --git a/src/Sentry/Protocol/Metrics/Metric.cs b/src/Sentry/Protocol/Metrics/Metric.cs index 0b94112d86..4c4e158d7d 100644 --- a/src/Sentry/Protocol/Metrics/Metric.cs +++ b/src/Sentry/Protocol/Metrics/Metric.cs @@ -5,20 +5,31 @@ namespace Sentry.Protocol.Metrics; internal abstract class Metric : IJsonSerializable { - public string Name { get; set; } = string.Empty; - public DateTime Timestamp { get; set; } = DateTime.Now; // TODO: Replace with constructor - public MeasurementUnit? Unit { get; set; } + protected Metric() : this(string.Empty) + { + } + + protected Metric(string key, MeasurementUnit? unit = null, IDictionary? tags = null) + { + Key = key; + Unit = unit; + Tags = tags ?? new Dictionary(); + } + + public string Key { get; private set; } + + public MeasurementUnit? Unit { get; private set; } + + public IDictionary Tags { get; private set; } - private readonly Lazy> _tags = new(() => new Dictionary()); - public IDictionary Tags => _tags.Value; + public abstract void Add(double value); - public abstract void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger); + protected abstract void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger); public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) { writer.WriteStartObject(); - writer.WriteString("name", Name.ToString()); - writer.WriteString("timestamp", Timestamp); + writer.WriteString("name", Key.ToString()); if (Unit.HasValue) { writer.WriteStringIfNotWhiteSpace("unit", Unit.ToString()); diff --git a/src/Sentry/Protocol/Metrics/SetMetric.cs b/src/Sentry/Protocol/Metrics/SetMetric.cs index e365391b1f..5cfd60430c 100644 --- a/src/Sentry/Protocol/Metrics/SetMetric.cs +++ b/src/Sentry/Protocol/Metrics/SetMetric.cs @@ -8,8 +8,24 @@ namespace Sentry.Protocol.Metrics; /// internal class SetMetric : Metric { - private HashSet Value { get; set; } = new(); + public SetMetric() + { + Value = new HashSet(); + } - public override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) => + public SetMetric(string key, int value, MeasurementUnit? unit = null, IDictionary? tags = null) + : base(key, unit, tags) + { + Value = new HashSet() { value }; + } + + public HashSet Value { get; private set; } + + public override void Add(double value) + { + throw new NotImplementedException(); + } + + protected override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) => writer.WriteArrayIfNotEmpty("value", Value, logger); } diff --git a/test/Sentry.Tests/MetricsAggregatorTests.cs b/test/Sentry.Tests/MetricsAggregatorTests.cs index d9408b78a3..d15c744e7a 100644 --- a/test/Sentry.Tests/MetricsAggregatorTests.cs +++ b/test/Sentry.Tests/MetricsAggregatorTests.cs @@ -1,3 +1,5 @@ +using Sentry.Protocol.Metrics; + namespace Sentry.Tests; public class MetricsAggregatorTests @@ -32,7 +34,7 @@ public void GetTimeBucketKey_RoundsDownToNearestTenSeconds(int seconds) public void GetMetricBucketKey_GeneratesExpectedKey() { // Arrange - var type = MetricType.Counter; + var type = MetricAggregator.MetricType.Counter; var metricKey = "quibbles"; var unit = MeasurementUnit.None; var tags = new Dictionary { ["tag1"] = "value1" }; @@ -45,53 +47,72 @@ public void GetMetricBucketKey_GeneratesExpectedKey() } [Fact] - public void Increment_NoMetric_CreatesBucketAndMetric() + public void Increment_AggregatesMetrics() { // Arrange + var metricType = MetricAggregator.MetricType.Counter; var key = "counter_key"; - var value = 5.0; var unit = MeasurementUnit.None; var tags = new Dictionary { ["tag1"] = "value1" }; - var timestamp = DateTime.UtcNow; - var sut = new MetricAggregator(); // Act - sut.Increment(key, value, unit, tags, timestamp); + DateTime firstTime = new(1970, 1, 1, 0, 0, 31, 0, DateTimeKind.Utc); + sut.Increment(key, 3, unit, tags, firstTime); + + DateTime secondTime = new(1970, 1, 1, 0, 0, 38, 0, DateTimeKind.Utc); + sut.Increment(key, 5, unit, tags, secondTime); + + DateTime thirdTime = new(1970, 1, 1, 0, 0, 40, 0, DateTimeKind.Utc); + sut.Increment(key, 13, unit, tags, thirdTime); // Assert - var timeBucket = sut.Buckets[MetricAggregator.GetTimeBucketKey(timestamp)]; - var metric = timeBucket[MetricAggregator.GetMetricBucketKey(MetricType.Counter, key, unit, tags)]; + var bucket1 = sut.Buckets[MetricAggregator.GetTimeBucketKey(firstTime)]; + var data1 = (CounterMetric)bucket1[MetricAggregator.GetMetricBucketKey(metricType, key, unit, tags)]; + data1.Value.Should().Be(8); // First two emits are in the same bucket - metric.Value.Should().Be(value); + var bucket2 = sut.Buckets[MetricAggregator.GetTimeBucketKey(thirdTime)]; + var data2 = (CounterMetric)bucket2[MetricAggregator.GetMetricBucketKey(metricType, key, unit, tags)]; + data2.Value.Should().Be(13); // First two emits are in the same bucket } [Fact] - public void Increment_MultipleMetrics_Aggregates() + public void Gauge_AggregatesMetrics() { // Arrange - var key = "counter_key"; + var metricType = MetricAggregator.MetricType.Gauge; + var key = "gauge_key"; var unit = MeasurementUnit.None; var tags = new Dictionary { ["tag1"] = "value1" }; var sut = new MetricAggregator(); // Act - DateTime firstTime = new(1970, 1, 1, 0, 0, 31, 0, DateTimeKind.Utc); - sut.Increment(key, 3, unit, tags, firstTime); + DateTime time1 = new(1970, 1, 1, 0, 0, 31, 0, DateTimeKind.Utc); + sut.Gauge(key, 3, unit, tags, time1); - DateTime secondTime = new(1970, 1, 1, 0, 0, 38, 0, DateTimeKind.Utc); - sut.Increment(key, 5, unit, tags, secondTime); + DateTime time2 = new(1970, 1, 1, 0, 0, 38, 0, DateTimeKind.Utc); + sut.Gauge(key, 5, unit, tags, time2); - DateTime thirdTime = new(1970, 1, 1, 0, 0, 40, 0, DateTimeKind.Utc); - sut.Increment(key, 13, unit, tags, thirdTime); + DateTime time3 = new(1970, 1, 1, 0, 0, 40, 0, DateTimeKind.Utc); + sut.Gauge(key, 13, unit, tags, time3); // Assert - var bucket1 = sut.Buckets[MetricAggregator.GetTimeBucketKey(firstTime)]; - var data1 = bucket1[MetricAggregator.GetMetricBucketKey(MetricType.Counter, key, unit, tags)]; - data1.Value.Should().Be(8); // First two emits are in the same bucket - - var bucket2 = sut.Buckets[MetricAggregator.GetTimeBucketKey(thirdTime)]; - var data2 = bucket2[MetricAggregator.GetMetricBucketKey(MetricType.Counter, key, unit, tags)]; - data2.Value.Should().Be(13); // First two emits are in the same bucket + var bucket1 = sut.Buckets[MetricAggregator.GetTimeBucketKey(time1)]; + var data1 = (GaugeMetric)bucket1[MetricAggregator.GetMetricBucketKey(metricType, key, unit, tags)]; + data1.Value.Should().Be(5); + data1.First.Should().Be(3); + data1.Min.Should().Be(3); + data1.Max.Should().Be(5); + data1.Sum.Should().Be(8); + data1.Count.Should().Be(2); + + var bucket2 = sut.Buckets[MetricAggregator.GetTimeBucketKey(time3)]; + var data2 = (GaugeMetric)bucket2[MetricAggregator.GetMetricBucketKey(metricType, key, unit, tags)]; + data2.Value.Should().Be(13); + data2.First.Should().Be(13); + data2.Min.Should().Be(13); + data2.Max.Should().Be(13); + data2.Sum.Should().Be(13); + data2.Count.Should().Be(1); } } From 9fac649b2243a8e684577fa189a4363beed74b3d Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 6 Dec 2023 16:25:31 +1300 Subject: [PATCH 05/52] Implemented Distribution and Set aggregations --- src/Sentry/Protocol/Metrics/SetMetric.cs | 2 +- test/Sentry.Tests/MetricsAggregatorTests.cs | 63 +++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/Sentry/Protocol/Metrics/SetMetric.cs b/src/Sentry/Protocol/Metrics/SetMetric.cs index 5cfd60430c..f18c0f6a0e 100644 --- a/src/Sentry/Protocol/Metrics/SetMetric.cs +++ b/src/Sentry/Protocol/Metrics/SetMetric.cs @@ -23,7 +23,7 @@ public SetMetric(string key, int value, MeasurementUnit? unit = null, IDictionar public override void Add(double value) { - throw new NotImplementedException(); + Value.Add((int)value); } protected override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) => diff --git a/test/Sentry.Tests/MetricsAggregatorTests.cs b/test/Sentry.Tests/MetricsAggregatorTests.cs index d15c744e7a..cf7da29c42 100644 --- a/test/Sentry.Tests/MetricsAggregatorTests.cs +++ b/test/Sentry.Tests/MetricsAggregatorTests.cs @@ -115,4 +115,67 @@ public void Gauge_AggregatesMetrics() data2.Sum.Should().Be(13); data2.Count.Should().Be(1); } + + [Fact] + public void Distribution_AggregatesMetrics() + { + // Arrange + var metricType = MetricAggregator.MetricType.Distribution; + var key = "distribution_key"; + var unit = MeasurementUnit.None; + var tags = new Dictionary { ["tag1"] = "value1" }; + var sut = new MetricAggregator(); + + // Act + DateTime time1 = new(1970, 1, 1, 0, 0, 31, 0, DateTimeKind.Utc); + sut.Distribution(key, 3, unit, tags, time1); + + DateTime time2 = new(1970, 1, 1, 0, 0, 38, 0, DateTimeKind.Utc); + sut.Distribution(key, 5, unit, tags, time2); + + DateTime time3 = new(1970, 1, 1, 0, 0, 40, 0, DateTimeKind.Utc); + sut.Distribution(key, 13, unit, tags, time3); + + // Assert + var bucket1 = sut.Buckets[MetricAggregator.GetTimeBucketKey(time1)]; + var data1 = (DistributionMetric)bucket1[MetricAggregator.GetMetricBucketKey(metricType, key, unit, tags)]; + data1.Value.Should().BeEquivalentTo(new[] {3, 5}); + + var bucket2 = sut.Buckets[MetricAggregator.GetTimeBucketKey(time3)]; + var data2 = (DistributionMetric)bucket2[MetricAggregator.GetMetricBucketKey(metricType, key, unit, tags)]; + data2.Value.Should().BeEquivalentTo(new[] {13}); + } + + [Fact] + public void Set_AggregatesMetrics() + { + // Arrange + var metricType = MetricAggregator.MetricType.Set; + var key = "set_key"; + var unit = MeasurementUnit.None; + var tags = new Dictionary { ["tag1"] = "value1" }; + var sut = new MetricAggregator(); + + // Act + DateTime time1 = new(1970, 1, 1, 0, 0, 31, 0, DateTimeKind.Utc); + sut.Set(key, 3, unit, tags, time1); + + DateTime time2 = new(1970, 1, 1, 0, 0, 38, 0, DateTimeKind.Utc); + sut.Set(key, 5, unit, tags, time2); + + DateTime time3 = new(1970, 1, 1, 0, 0, 40, 0, DateTimeKind.Utc); + sut.Set(key, 13, unit, tags, time3); + + DateTime time4 = new(1970, 1, 1, 0, 0, 42, 0, DateTimeKind.Utc); + sut.Set(key, 13, unit, tags, time3); + + // Assert + var bucket1 = sut.Buckets[MetricAggregator.GetTimeBucketKey(time1)]; + var data1 = (SetMetric)bucket1[MetricAggregator.GetMetricBucketKey(metricType, key, unit, tags)]; + data1.Value.Should().BeEquivalentTo(new[] {3, 5}); + + var bucket2 = sut.Buckets[MetricAggregator.GetTimeBucketKey(time3)]; + var data2 = (SetMetric)bucket2[MetricAggregator.GetMetricBucketKey(metricType, key, unit, tags)]; + data2.Value.Should().BeEquivalentTo(new[] {13}); + } } From c65355dede946090646be759d5a39f3df19fd4fb Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 6 Dec 2023 21:36:35 +1300 Subject: [PATCH 06/52] Added Metrics to ISentryClient API --- src/Sentry/DelegatingMetricAggregator.cs | 16 ++++ src/Sentry/DisabledMetricAggregator.cs | 28 +++++++ src/Sentry/Extensibility/DisabledHub.cs | 5 ++ src/Sentry/Extensibility/HubAdapter.cs | 4 + src/Sentry/IMetricAggregator.cs | 76 +++++++++++++++++++ src/Sentry/ISentryClient.cs | 5 ++ src/Sentry/Internal/Hub.cs | 10 +++ src/Sentry/MetricAggregator.cs | 38 ++-------- src/Sentry/Protocol/Envelopes/Envelope.cs | 16 ++++ src/Sentry/Protocol/Envelopes/EnvelopeItem.cs | 15 ++++ src/Sentry/SentryClient.cs | 22 ++++++ src/Sentry/SentryOptions.cs | 25 ++++++ src/Sentry/SentrySdk.cs | 4 + .../BindableSentryAspNetCoreOptionsTests.cs | 4 + ...indableSentryAzureFunctionsOptionsTests.cs | 4 + .../SentryLoggingOptionsTests.cs | 4 + .../BindableSentryAspNetCoreOptionsTests.cs | 4 + ...piApprovalTests.Run.DotNet6_0.verified.txt | 17 +++++ ...piApprovalTests.Run.DotNet7_0.verified.txt | 17 +++++ ...piApprovalTests.Run.DotNet8_0.verified.txt | 17 +++++ .../ApiApprovalTests.Run.Net4_8.verified.txt | 17 +++++ .../BindableSentryOptionsTests.cs | 4 + ...gatorTests.cs => MetricAggregatorTests.cs} | 2 +- 23 files changed, 320 insertions(+), 34 deletions(-) create mode 100644 src/Sentry/DelegatingMetricAggregator.cs create mode 100644 src/Sentry/DisabledMetricAggregator.cs create mode 100644 src/Sentry/IMetricAggregator.cs rename test/Sentry.Tests/{MetricsAggregatorTests.cs => MetricAggregatorTests.cs} (99%) diff --git a/src/Sentry/DelegatingMetricAggregator.cs b/src/Sentry/DelegatingMetricAggregator.cs new file mode 100644 index 0000000000..0f3e0b0f82 --- /dev/null +++ b/src/Sentry/DelegatingMetricAggregator.cs @@ -0,0 +1,16 @@ +namespace Sentry; + +internal class DelegatingMetricAggregator(IMetricAggregator innerAggregator) : IMetricAggregator +{ + public void Increment(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, + DateTime? timestamp = null) => innerAggregator.Increment(key, value, unit, tags, timestamp); + + public void Gauge(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, + DateTime? timestamp = null) => innerAggregator.Gauge(key, value, unit, tags, timestamp); + + public void Distribution(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, + DateTime? timestamp = null) => innerAggregator.Distribution(key, value, unit, tags, timestamp); + + public void Set(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, + DateTime? timestamp = null) => innerAggregator.Set(key, value, unit, tags, timestamp); +} diff --git a/src/Sentry/DisabledMetricAggregator.cs b/src/Sentry/DisabledMetricAggregator.cs new file mode 100644 index 0000000000..e087d700cf --- /dev/null +++ b/src/Sentry/DisabledMetricAggregator.cs @@ -0,0 +1,28 @@ +namespace Sentry; + +internal class DisabledMetricAggregator : IMetricAggregator +{ + public void Increment(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, + DateTime? timestamp = null) + { + // No Op + } + + public void Gauge(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, + DateTime? timestamp = null) + { + // No Op + } + + public void Distribution(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, + DateTime? timestamp = null) + { + // No Op + } + + public void Set(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, + DateTime? timestamp = null) + { + // No Op + } +} diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs index c41ea7121e..3ce135f59a 100644 --- a/src/Sentry/Extensibility/DisabledHub.cs +++ b/src/Sentry/Extensibility/DisabledHub.cs @@ -174,6 +174,11 @@ public void CaptureSession(SessionUpdate sessionUpdate) /// public Task FlushAsync(TimeSpan timeout) => Task.CompletedTask; + /// + /// Disabled Metrics Aggregator (all methods are no-op). + /// + public IMetricAggregator Metrics { get; } = new DisabledMetricAggregator(); + /// /// No-Op. /// diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs index 6f7cbaa4d1..072a6af077 100644 --- a/src/Sentry/Extensibility/HubAdapter.cs +++ b/src/Sentry/Extensibility/HubAdapter.cs @@ -271,6 +271,10 @@ public void CaptureSession(SessionUpdate sessionUpdate) public Task FlushAsync(TimeSpan timeout) => SentrySdk.FlushAsync(timeout); + /// + public IMetricAggregator Metrics + => SentrySdk.Metrics; + /// /// Forwards the call to /// diff --git a/src/Sentry/IMetricAggregator.cs b/src/Sentry/IMetricAggregator.cs new file mode 100644 index 0000000000..754eeb9539 --- /dev/null +++ b/src/Sentry/IMetricAggregator.cs @@ -0,0 +1,76 @@ +namespace Sentry; + +/// +/// Exposes EXPERIMENTAL capability to emit metrics. This API is subject to change without major version bumps so use +/// with caution. We advise disabling in production at the moment. +/// +public interface IMetricAggregator +{ + /// + /// Emits a Counter metric + /// + /// A unique key identifying the metric + /// The value to be added + /// An optional + /// Optional Tags to associate with the metric + /// The time when the metric was emitted + void Increment( + string key, + double value = 1.0, + MeasurementUnit? unit = null, + IDictionary? tags = null, + DateTime? timestamp = null + // , int stacklevel = 0 // Used for code locations + ); + + /// + /// Emits a Gauge metric + /// + /// A unique key identifying the metric + /// The value to be added + /// An optional + /// Optional Tags to associate with the metric + /// The time when the metric was emitted + void Gauge( + string key, + double value = 1.0, + MeasurementUnit? unit = null, + IDictionary? tags = null, + DateTime? timestamp = null + // , int stacklevel = 0 // Used for code locations + ); + + /// + /// Emits a Distribution metric + /// + /// A unique key identifying the metric + /// The value to be added + /// An optional + /// Optional Tags to associate with the metric + /// The time when the metric was emitted + void Distribution( + string key, + double value = 1.0, + MeasurementUnit? unit = null, + IDictionary? tags = null, + DateTime? timestamp = null + // , int stacklevel = 0 // Used for code locations + ); + + /// + /// Emits a Set metric + /// + /// A unique key identifying the metric + /// The value to be added + /// An optional + /// Optional Tags to associate with the metric + /// The time when the metric was emitted + void Set( + string key, + double value = 1.0, + MeasurementUnit? unit = null, + IDictionary? tags = null, + DateTime? timestamp = null + // , int stacklevel = 0 // Used for code locations + ); +} diff --git a/src/Sentry/ISentryClient.cs b/src/Sentry/ISentryClient.cs index 0b1d193225..237f3e04ff 100644 --- a/src/Sentry/ISentryClient.cs +++ b/src/Sentry/ISentryClient.cs @@ -68,4 +68,9 @@ public interface ISentryClient /// The amount of time allowed for flushing. /// A task to await for the flush operation. Task FlushAsync(TimeSpan timeout); + + /// + /// + /// + IMetricAggregator Metrics { get; } } diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index c8c2e1da86..82bee8dfd5 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -2,6 +2,7 @@ using Sentry.Infrastructure; using Sentry.Integrations; using Sentry.Internal.ScopeStack; +using Sentry.Protocol.Metrics; namespace Sentry.Internal; @@ -22,6 +23,11 @@ internal class Hub : IHub, IDisposable internal IInternalScopeManager ScopeManager { get; } + /// + /// + /// + public IMetricAggregator Metrics { get; } + private int _isEnabled = 1; public bool IsEnabled => _isEnabled == 1; @@ -58,6 +64,10 @@ internal Hub( PushScope(); } + Metrics = _ownedClient is SentryClient sentryClient + ? new DelegatingMetricAggregator(sentryClient.Metrics) + : new DisabledMetricAggregator(); + foreach (var integration in options.Integrations) { options.LogDebug("Registering integration: '{0}'.", integration.GetType().Name); diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index 4d8b637235..b5df173111 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -3,7 +3,7 @@ namespace Sentry; -internal class MetricAggregator +internal class MetricAggregator() : IMetricAggregator { private const int RollupInSeconds = 10; private readonly TimeSpan _flushInterval = TimeSpan.FromSeconds(5); @@ -55,14 +55,7 @@ internal static string GetMetricBucketKey(MetricType type, string metricKey, Mea return $"{typePrefix}_{metricKey}_{unit}_{serializedTags}"; } - /// - /// Emits a Counter metric - /// - /// A unique key identifying the metric - /// The value to be added - /// An optional - /// Optional Tags to associate with the metric - /// The time when the metric was emitted + /// public void Increment( string key, double value = 1.0, @@ -72,14 +65,7 @@ public void Increment( // , int stacklevel = 0 // Used for code locations ) => Emit(MetricType.Counter, key, value, unit, tags, timestamp); - /// - /// Emits a Gauge metric - /// - /// A unique key identifying the metric - /// The value to be added - /// An optional - /// Optional Tags to associate with the metric - /// The time when the metric was emitted + /// public void Gauge( string key, double value = 1.0, @@ -89,14 +75,7 @@ public void Gauge( // , int stacklevel = 0 // Used for code locations ) => Emit(MetricType.Gauge, key, value, unit, tags, timestamp); - /// - /// Emits a Distribution metric - /// - /// A unique key identifying the metric - /// The value to be added - /// An optional - /// Optional Tags to associate with the metric - /// The time when the metric was emitted + /// public void Distribution( string key, double value = 1.0, @@ -106,14 +85,7 @@ public void Distribution( // , int stacklevel = 0 // Used for code locations ) => Emit(MetricType.Distribution, key, value, unit, tags, timestamp); - /// - /// Emits a Set metric - /// - /// A unique key identifying the metric - /// The value to be added - /// An optional - /// Optional Tags to associate with the metric - /// The time when the metric was emitted + /// public void Set( string key, double value = 1.0, diff --git a/src/Sentry/Protocol/Envelopes/Envelope.cs b/src/Sentry/Protocol/Envelopes/Envelope.cs index 488f07ed93..9b6bf1b6c6 100644 --- a/src/Sentry/Protocol/Envelopes/Envelope.cs +++ b/src/Sentry/Protocol/Envelopes/Envelope.cs @@ -2,6 +2,7 @@ using Sentry.Infrastructure; using Sentry.Internal; using Sentry.Internal.Extensions; +using Sentry.Protocol.Metrics; namespace Sentry.Protocol.Envelopes; @@ -320,6 +321,21 @@ public static Envelope FromTransaction(Transaction transaction) return new Envelope(eventId, header, items); } + /// + /// Creates an envelope that contains a + /// + internal static Envelope FromMetric(Metric metric) + { + var header = DefaultHeader; + + var items = new[] + { + EnvelopeItem.FromMetric(metric) + }; + + return new Envelope(header, items); + } + /// /// Creates an envelope that contains a session update. /// diff --git a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs index 022fb75dcc..13020583db 100644 --- a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs +++ b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs @@ -1,6 +1,7 @@ using Sentry.Extensibility; using Sentry.Internal; using Sentry.Internal.Extensions; +using Sentry.Protocol.Metrics; namespace Sentry.Protocol.Envelopes; @@ -18,6 +19,7 @@ public sealed class EnvelopeItem : ISerializable, IDisposable private const string TypeValueAttachment = "attachment"; private const string TypeValueClientReport = "client_report"; private const string TypeValueProfile = "profile"; + private const string TypeValueMetric = "statsd"; private const string LengthKey = "length"; private const string FileNameKey = "filename"; @@ -221,6 +223,19 @@ public static EnvelopeItem FromTransaction(Transaction transaction) return new EnvelopeItem(header, new JsonSerializable(transaction)); } + /// + /// Creates an from . + /// + internal static EnvelopeItem FromMetric(Metric metric) + { + var header = new Dictionary(1, StringComparer.Ordinal) + { + [TypeKey] = TypeValueMetric + }; + + return new EnvelopeItem(header, new JsonSerializable(metric)); + } + /// /// Creates an from . /// diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index 3f39a34f40..fda6de8ba5 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -1,6 +1,7 @@ using Sentry.Extensibility; using Sentry.Internal; using Sentry.Protocol.Envelopes; +using Sentry.Protocol.Metrics; namespace Sentry; @@ -21,6 +22,12 @@ public class SentryClient : ISentryClient, IDisposable private readonly Enricher _enricher; internal IBackgroundWorker Worker { get; } + + /// + /// + /// + public IMetricAggregator Metrics { get; } + internal SentryOptions Options => _options; /// @@ -67,6 +74,15 @@ internal SentryClient( options.LogDebug("Worker of type {0} was provided via Options.", worker.GetType().Name); Worker = worker; } + + if (options.ExperimentalMetrics is { MetricSampleRate: > 0 } experimentalMetricsOptions) + { + Metrics = new MetricAggregator(); + } + else + { + Metrics = new DisabledMetricAggregator(); + } } /// @@ -222,6 +238,12 @@ public void CaptureTransaction(Transaction transaction, Scope? scope, Hint? hint return transaction; } + /// + internal void CaptureMetric(Metric metric) + { + CaptureEnvelope(Envelope.FromMetric(metric)); + } + /// public void CaptureSession(SessionUpdate sessionUpdate) { diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index 9a8aedb66a..42003c3679 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -1064,6 +1064,18 @@ public bool JsonPreserveReferences [EditorBrowsable(EditorBrowsableState.Never)] public Func? AssemblyReader { get; set; } + /// + /// + /// Settings for the EXPERIMENTAL metrics feature. This feature is preview only and subject to change without a + /// major version bump. Currently it's recommended for noodling only - DON'T USE IN PRODUCTION! + /// + /// + /// By default the ExperimentalMetrics Options is null, which means the feature is disabled. If you want to enable + /// Experimental metrics, you must set this property to a non-null value. + /// + /// + public ExperimentalMetricsOptions? ExperimentalMetrics { get; set; } + internal SettingLocator SettingLocator { get; set; } /// @@ -1231,3 +1243,16 @@ internal enum DefaultIntegrations #endif } } + +/// +/// Settings to the experimental Metrics feature. This feature is preview only and will very likely change in the future +/// without a major version bump... so use at your own risk. +/// +public class ExperimentalMetricsOptions +{ + /// + /// Determines the sample rate for metrics. 0.0 means no metrics will be sent (metrics disabled). 1.0 implies all + /// metrics will be sent. + /// + public double MetricSampleRate { get; set; } = 0; +} diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index 4f04eff69c..5ea99cf205 100644 --- a/src/Sentry/SentrySdk.cs +++ b/src/Sentry/SentrySdk.cs @@ -590,6 +590,10 @@ public static TransactionContext ContinueTrace( string? operation = null) => CurrentHub.ContinueTrace(traceHeader, baggageHeader, name, operation); + /// + public static IMetricAggregator Metrics + => CurrentHub.Metrics; + /// [DebuggerStepThrough] public static void StartSession() diff --git a/test/Sentry.AspNetCore.Tests/BindableSentryAspNetCoreOptionsTests.cs b/test/Sentry.AspNetCore.Tests/BindableSentryAspNetCoreOptionsTests.cs index 0df315c7b2..b832f0607d 100644 --- a/test/Sentry.AspNetCore.Tests/BindableSentryAspNetCoreOptionsTests.cs +++ b/test/Sentry.AspNetCore.Tests/BindableSentryAspNetCoreOptionsTests.cs @@ -5,6 +5,10 @@ namespace Sentry.AspNetCore.Tests; public class BindableSentryAspNetCoreOptionsTests: BindableTests { + public BindableSentryAspNetCoreOptionsTests() : base(nameof(SentryAspNetCoreOptions.ExperimentalMetrics)) + { + } + [Fact] public void BindableProperties_MatchOptionsProperties() { diff --git a/test/Sentry.Azure.Functions.Worker.Tests/BindableSentryAzureFunctionsOptionsTests.cs b/test/Sentry.Azure.Functions.Worker.Tests/BindableSentryAzureFunctionsOptionsTests.cs index c225daa771..5d21a7d3a4 100644 --- a/test/Sentry.Azure.Functions.Worker.Tests/BindableSentryAzureFunctionsOptionsTests.cs +++ b/test/Sentry.Azure.Functions.Worker.Tests/BindableSentryAzureFunctionsOptionsTests.cs @@ -5,6 +5,10 @@ namespace Sentry.Azure.Functions.Worker.Tests; public class BindableSentryAzureFunctionsOptionsTests: BindableTests { + public BindableSentryAzureFunctionsOptionsTests() : base(nameof(SentryAzureFunctionsOptions.ExperimentalMetrics)) + { + } + [Fact] public void BindableProperties_MatchOptionsProperties() { diff --git a/test/Sentry.Extensions.Logging.Tests/SentryLoggingOptionsTests.cs b/test/Sentry.Extensions.Logging.Tests/SentryLoggingOptionsTests.cs index 772473b267..26ba0fb740 100644 --- a/test/Sentry.Extensions.Logging.Tests/SentryLoggingOptionsTests.cs +++ b/test/Sentry.Extensions.Logging.Tests/SentryLoggingOptionsTests.cs @@ -5,6 +5,10 @@ namespace Sentry.Extensions.Logging.Tests; public class SentryLoggingOptionsTests : BindableTests { + public SentryLoggingOptionsTests() : base(nameof(SentryLoggingOptions.ExperimentalMetrics)) + { + } + [Fact] public void BindableProperties_MatchOptionsProperties() { diff --git a/test/Sentry.Maui.Tests/BindableSentryAspNetCoreOptionsTests.cs b/test/Sentry.Maui.Tests/BindableSentryAspNetCoreOptionsTests.cs index a94421c9e8..821f529746 100644 --- a/test/Sentry.Maui.Tests/BindableSentryAspNetCoreOptionsTests.cs +++ b/test/Sentry.Maui.Tests/BindableSentryAspNetCoreOptionsTests.cs @@ -5,6 +5,10 @@ namespace Sentry.Maui.Tests; public class BindableSentryMauiOptionsTests: BindableTests { + public BindableSentryMauiOptionsTests() : base(nameof(SentryMauiOptions.ExperimentalMetrics)) + { + } + [Fact] public void BindableProperties_MatchOptionsProperties() { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt index 60d2cee9db..7383a792a8 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt @@ -126,6 +126,11 @@ namespace Sentry public static void SetFingerprint(this Sentry.IEventLike eventLike, System.Collections.Generic.IEnumerable fingerprint) { } public static void SetFingerprint(this Sentry.IEventLike eventLike, params string[] fingerprint) { } } + public class ExperimentalMetricsOptions + { + public ExperimentalMetricsOptions() { } + public double MetricSampleRate { get; set; } + } public class FileAttachmentContent : Sentry.IAttachmentContent { public FileAttachmentContent(string filePath) { } @@ -237,6 +242,13 @@ namespace Sentry { void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger); } + public interface IMetricAggregator + { + void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); + void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); + void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); + void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); + } public interface IScopeObserver { void AddBreadcrumb(Sentry.Breadcrumb breadcrumb); @@ -248,6 +260,7 @@ namespace Sentry public interface ISentryClient { bool IsEnabled { get; } + Sentry.IMetricAggregator Metrics { get; } Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.Hint? hint = null); void CaptureSession(Sentry.SessionUpdate sessionUpdate); void CaptureTransaction(Sentry.Transaction transaction); @@ -475,6 +488,7 @@ namespace Sentry { public SentryClient(Sentry.SentryOptions options) { } public bool IsEnabled { get; } + public Sentry.IMetricAggregator Metrics { get; } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent? @event, Sentry.Scope? scope = null, Sentry.Hint? hint = null) { } public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.Transaction transaction) { } @@ -620,6 +634,7 @@ namespace Sentry public bool EnableScopeSync { get; set; } public bool? EnableTracing { get; set; } public string? Environment { get; set; } + public Sentry.ExperimentalMetricsOptions? ExperimentalMetrics { get; set; } public System.Collections.Generic.IList FailedRequestStatusCodes { get; set; } public System.Collections.Generic.IList FailedRequestTargets { get; set; } public System.TimeSpan FlushTimeout { get; set; } @@ -1180,6 +1195,7 @@ namespace Sentry.Extensibility public static readonly Sentry.Extensibility.DisabledHub Instance; public bool IsEnabled { get; } public Sentry.SentryId LastEventId { get; } + public Sentry.IMetricAggregator Metrics { get; } public void BindClient(Sentry.ISentryClient client) { } public void BindException(System.Exception exception, Sentry.ISpan span) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope) { } @@ -1217,6 +1233,7 @@ namespace Sentry.Extensibility public static readonly Sentry.Extensibility.HubAdapter Instance; public bool IsEnabled { get; } public Sentry.SentryId LastEventId { get; } + public Sentry.IMetricAggregator Metrics { get; } public void AddBreadcrumb(string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void AddBreadcrumb(Sentry.Infrastructure.ISystemClock clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void BindClient(Sentry.ISentryClient client) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt index 60d2cee9db..7383a792a8 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt @@ -126,6 +126,11 @@ namespace Sentry public static void SetFingerprint(this Sentry.IEventLike eventLike, System.Collections.Generic.IEnumerable fingerprint) { } public static void SetFingerprint(this Sentry.IEventLike eventLike, params string[] fingerprint) { } } + public class ExperimentalMetricsOptions + { + public ExperimentalMetricsOptions() { } + public double MetricSampleRate { get; set; } + } public class FileAttachmentContent : Sentry.IAttachmentContent { public FileAttachmentContent(string filePath) { } @@ -237,6 +242,13 @@ namespace Sentry { void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger); } + public interface IMetricAggregator + { + void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); + void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); + void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); + void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); + } public interface IScopeObserver { void AddBreadcrumb(Sentry.Breadcrumb breadcrumb); @@ -248,6 +260,7 @@ namespace Sentry public interface ISentryClient { bool IsEnabled { get; } + Sentry.IMetricAggregator Metrics { get; } Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.Hint? hint = null); void CaptureSession(Sentry.SessionUpdate sessionUpdate); void CaptureTransaction(Sentry.Transaction transaction); @@ -475,6 +488,7 @@ namespace Sentry { public SentryClient(Sentry.SentryOptions options) { } public bool IsEnabled { get; } + public Sentry.IMetricAggregator Metrics { get; } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent? @event, Sentry.Scope? scope = null, Sentry.Hint? hint = null) { } public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.Transaction transaction) { } @@ -620,6 +634,7 @@ namespace Sentry public bool EnableScopeSync { get; set; } public bool? EnableTracing { get; set; } public string? Environment { get; set; } + public Sentry.ExperimentalMetricsOptions? ExperimentalMetrics { get; set; } public System.Collections.Generic.IList FailedRequestStatusCodes { get; set; } public System.Collections.Generic.IList FailedRequestTargets { get; set; } public System.TimeSpan FlushTimeout { get; set; } @@ -1180,6 +1195,7 @@ namespace Sentry.Extensibility public static readonly Sentry.Extensibility.DisabledHub Instance; public bool IsEnabled { get; } public Sentry.SentryId LastEventId { get; } + public Sentry.IMetricAggregator Metrics { get; } public void BindClient(Sentry.ISentryClient client) { } public void BindException(System.Exception exception, Sentry.ISpan span) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope) { } @@ -1217,6 +1233,7 @@ namespace Sentry.Extensibility public static readonly Sentry.Extensibility.HubAdapter Instance; public bool IsEnabled { get; } public Sentry.SentryId LastEventId { get; } + public Sentry.IMetricAggregator Metrics { get; } public void AddBreadcrumb(string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void AddBreadcrumb(Sentry.Infrastructure.ISystemClock clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void BindClient(Sentry.ISentryClient client) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 4ed6442010..5e4e89b357 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -127,6 +127,11 @@ namespace Sentry public static void SetFingerprint(this Sentry.IEventLike eventLike, System.Collections.Generic.IEnumerable fingerprint) { } public static void SetFingerprint(this Sentry.IEventLike eventLike, params string[] fingerprint) { } } + public class ExperimentalMetricsOptions + { + public ExperimentalMetricsOptions() { } + public double MetricSampleRate { get; set; } + } public class FileAttachmentContent : Sentry.IAttachmentContent { public FileAttachmentContent(string filePath) { } @@ -238,6 +243,13 @@ namespace Sentry { void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger); } + public interface IMetricAggregator + { + void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); + void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); + void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); + void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); + } public interface IScopeObserver { void AddBreadcrumb(Sentry.Breadcrumb breadcrumb); @@ -249,6 +261,7 @@ namespace Sentry public interface ISentryClient { bool IsEnabled { get; } + Sentry.IMetricAggregator Metrics { get; } Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.Hint? hint = null); void CaptureSession(Sentry.SessionUpdate sessionUpdate); void CaptureTransaction(Sentry.Transaction transaction); @@ -476,6 +489,7 @@ namespace Sentry { public SentryClient(Sentry.SentryOptions options) { } public bool IsEnabled { get; } + public Sentry.IMetricAggregator Metrics { get; } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent? @event, Sentry.Scope? scope = null, Sentry.Hint? hint = null) { } public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.Transaction transaction) { } @@ -621,6 +635,7 @@ namespace Sentry public bool EnableScopeSync { get; set; } public bool? EnableTracing { get; set; } public string? Environment { get; set; } + public Sentry.ExperimentalMetricsOptions? ExperimentalMetrics { get; set; } public System.Collections.Generic.IList FailedRequestStatusCodes { get; set; } public System.Collections.Generic.IList FailedRequestTargets { get; set; } public System.TimeSpan FlushTimeout { get; set; } @@ -1181,6 +1196,7 @@ namespace Sentry.Extensibility public static readonly Sentry.Extensibility.DisabledHub Instance; public bool IsEnabled { get; } public Sentry.SentryId LastEventId { get; } + public Sentry.IMetricAggregator Metrics { get; } public void BindClient(Sentry.ISentryClient client) { } public void BindException(System.Exception exception, Sentry.ISpan span) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope) { } @@ -1218,6 +1234,7 @@ namespace Sentry.Extensibility public static readonly Sentry.Extensibility.HubAdapter Instance; public bool IsEnabled { get; } public Sentry.SentryId LastEventId { get; } + public Sentry.IMetricAggregator Metrics { get; } public void AddBreadcrumb(string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void AddBreadcrumb(Sentry.Infrastructure.ISystemClock clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void BindClient(Sentry.ISentryClient client) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index c2ad004e64..5cf2c68f44 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -125,6 +125,11 @@ namespace Sentry public static void SetFingerprint(this Sentry.IEventLike eventLike, System.Collections.Generic.IEnumerable fingerprint) { } public static void SetFingerprint(this Sentry.IEventLike eventLike, params string[] fingerprint) { } } + public class ExperimentalMetricsOptions + { + public ExperimentalMetricsOptions() { } + public double MetricSampleRate { get; set; } + } public class FileAttachmentContent : Sentry.IAttachmentContent { public FileAttachmentContent(string filePath) { } @@ -236,6 +241,13 @@ namespace Sentry { void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger); } + public interface IMetricAggregator + { + void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); + void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); + void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); + void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); + } public interface IScopeObserver { void AddBreadcrumb(Sentry.Breadcrumb breadcrumb); @@ -247,6 +259,7 @@ namespace Sentry public interface ISentryClient { bool IsEnabled { get; } + Sentry.IMetricAggregator Metrics { get; } Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.Hint? hint = null); void CaptureSession(Sentry.SessionUpdate sessionUpdate); void CaptureTransaction(Sentry.Transaction transaction); @@ -474,6 +487,7 @@ namespace Sentry { public SentryClient(Sentry.SentryOptions options) { } public bool IsEnabled { get; } + public Sentry.IMetricAggregator Metrics { get; } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent? @event, Sentry.Scope? scope = null, Sentry.Hint? hint = null) { } public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.Transaction transaction) { } @@ -618,6 +632,7 @@ namespace Sentry public bool EnableScopeSync { get; set; } public bool? EnableTracing { get; set; } public string? Environment { get; set; } + public Sentry.ExperimentalMetricsOptions? ExperimentalMetrics { get; set; } public System.Collections.Generic.IList FailedRequestStatusCodes { get; set; } public System.Collections.Generic.IList FailedRequestTargets { get; set; } public System.TimeSpan FlushTimeout { get; set; } @@ -1177,6 +1192,7 @@ namespace Sentry.Extensibility public static readonly Sentry.Extensibility.DisabledHub Instance; public bool IsEnabled { get; } public Sentry.SentryId LastEventId { get; } + public Sentry.IMetricAggregator Metrics { get; } public void BindClient(Sentry.ISentryClient client) { } public void BindException(System.Exception exception, Sentry.ISpan span) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope) { } @@ -1214,6 +1230,7 @@ namespace Sentry.Extensibility public static readonly Sentry.Extensibility.HubAdapter Instance; public bool IsEnabled { get; } public Sentry.SentryId LastEventId { get; } + public Sentry.IMetricAggregator Metrics { get; } public void AddBreadcrumb(string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void AddBreadcrumb(Sentry.Infrastructure.ISystemClock clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public void BindClient(Sentry.ISentryClient client) { } diff --git a/test/Sentry.Tests/BindableSentryOptionsTests.cs b/test/Sentry.Tests/BindableSentryOptionsTests.cs index 50f39e954a..977f8570d8 100644 --- a/test/Sentry.Tests/BindableSentryOptionsTests.cs +++ b/test/Sentry.Tests/BindableSentryOptionsTests.cs @@ -5,6 +5,10 @@ namespace Sentry.Tests; public class BindableSentryOptionsTests : BindableTests { + public BindableSentryOptionsTests() : base(nameof(SentryOptions.ExperimentalMetrics)) + { + } + [Fact] public void BindableProperties_MatchOptionsProperties() { diff --git a/test/Sentry.Tests/MetricsAggregatorTests.cs b/test/Sentry.Tests/MetricAggregatorTests.cs similarity index 99% rename from test/Sentry.Tests/MetricsAggregatorTests.cs rename to test/Sentry.Tests/MetricAggregatorTests.cs index cf7da29c42..d35ab33bd8 100644 --- a/test/Sentry.Tests/MetricsAggregatorTests.cs +++ b/test/Sentry.Tests/MetricAggregatorTests.cs @@ -2,7 +2,7 @@ namespace Sentry.Tests; -public class MetricsAggregatorTests +public class MetricAggregatorTests { class Fixture { From b77c04d92bf4d77fa641260a4e84c1fedc8d92aa Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 6 Dec 2023 21:45:47 +1300 Subject: [PATCH 07/52] Update Hub.cs --- src/Sentry/Internal/Hub.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 82bee8dfd5..4b54241c9e 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -23,9 +23,7 @@ internal class Hub : IHub, IDisposable internal IInternalScopeManager ScopeManager { get; } - /// /// - /// public IMetricAggregator Metrics { get; } private int _isEnabled = 1; @@ -64,9 +62,7 @@ internal Hub( PushScope(); } - Metrics = _ownedClient is SentryClient sentryClient - ? new DelegatingMetricAggregator(sentryClient.Metrics) - : new DisabledMetricAggregator(); + Metrics = new DelegatingMetricAggregator(_ownedClient.Metrics); foreach (var integration in options.Integrations) { From c0233db2f297357c5f9e95eb7f54b113b30f96e7 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 6 Dec 2023 22:06:33 +1300 Subject: [PATCH 08/52] Verify tests --- test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt | 1 + test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt | 1 + test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt | 1 + test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt | 1 + 4 files changed, 4 insertions(+) diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt index 7383a792a8..e13150c4b7 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt @@ -717,6 +717,7 @@ namespace Sentry { public static bool IsEnabled { get; } public static Sentry.SentryId LastEventId { get; } + public static Sentry.IMetricAggregator Metrics { get; } public static void AddBreadcrumb(Sentry.Breadcrumb breadcrumb, Sentry.Hint? hint = null) { } public static void AddBreadcrumb(string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public static void AddBreadcrumb(Sentry.Infrastructure.ISystemClock? clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt index 7383a792a8..e13150c4b7 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt @@ -717,6 +717,7 @@ namespace Sentry { public static bool IsEnabled { get; } public static Sentry.SentryId LastEventId { get; } + public static Sentry.IMetricAggregator Metrics { get; } public static void AddBreadcrumb(Sentry.Breadcrumb breadcrumb, Sentry.Hint? hint = null) { } public static void AddBreadcrumb(string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public static void AddBreadcrumb(Sentry.Infrastructure.ISystemClock? clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 5e4e89b357..531b2da2d9 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -718,6 +718,7 @@ namespace Sentry { public static bool IsEnabled { get; } public static Sentry.SentryId LastEventId { get; } + public static Sentry.IMetricAggregator Metrics { get; } public static void AddBreadcrumb(Sentry.Breadcrumb breadcrumb, Sentry.Hint? hint = null) { } public static void AddBreadcrumb(string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public static void AddBreadcrumb(Sentry.Infrastructure.ISystemClock? clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 5cf2c68f44..1819d1cd74 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -714,6 +714,7 @@ namespace Sentry { public static bool IsEnabled { get; } public static Sentry.SentryId LastEventId { get; } + public static Sentry.IMetricAggregator Metrics { get; } public static void AddBreadcrumb(Sentry.Breadcrumb breadcrumb, Sentry.Hint? hint = null) { } public static void AddBreadcrumb(string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public static void AddBreadcrumb(Sentry.Infrastructure.ISystemClock? clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } From 3b74b80e5902dbbcf6763b0ad2b883357dffbd3d Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 7 Dec 2023 11:10:41 +1300 Subject: [PATCH 09/52] Basic flush loop (no tests yet) --- src/Sentry/MetricAggregator.cs | 219 ++++++++++++++---- src/Sentry/Protocol/Envelopes/Envelope.cs | 9 +- src/Sentry/Protocol/Metrics/CounterMetric.cs | 10 +- .../Protocol/Metrics/DistributionMetric.cs | 6 +- src/Sentry/Protocol/Metrics/GaugeMetric.cs | 7 +- src/Sentry/Protocol/Metrics/Metric.cs | 11 +- src/Sentry/Protocol/Metrics/SetMetric.cs | 7 +- src/Sentry/SentryClient.cs | 10 +- test/Sentry.Tests/MetricAggregatorTests.cs | 13 +- 9 files changed, 221 insertions(+), 71 deletions(-) diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index b5df173111..966b0c732d 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -1,15 +1,21 @@ +using Sentry.Extensibility; using Sentry.Internal.Extensions; using Sentry.Protocol.Metrics; namespace Sentry; -internal class MetricAggregator() : IMetricAggregator +internal class MetricAggregator : IMetricAggregator, IDisposable { + internal enum MetricType : byte { Counter, Gauge, Distribution, Set } + private const int RollupInSeconds = 10; - private readonly TimeSpan _flushInterval = TimeSpan.FromSeconds(5); - internal enum MetricType : byte { Counter, Gauge, Distribution, Set } + private readonly SentryOptions _options; + private readonly Action> _captureMetrics; + private readonly TimeSpan _flushInterval = TimeSpan.FromSeconds(5); + private readonly CancellationTokenSource _shutdownSource; + private volatile bool _disposed; // 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, @@ -18,18 +24,26 @@ internal enum MetricType : byte { Counter, Gauge, Distribution, Set } private readonly Lazy>> _buckets = new(() => new ConcurrentDictionary>()); - // private readonly Timer _flushTimer; - // private readonly Action> _onFlush; + private Task LoopTask { get; } - // public MetricsAggregator(TimeSpan? flushInterval, Action> onFlush) - // { - // if (flushInterval.HasValue) - // { - // _flushInterval = flushInterval.Value; - // } - // _onFlush = onFlush; - // _flushTimer = new Timer(FlushData, null, _flushInterval, _flushInterval); - // } + public MetricAggregator(SentryOptions options, Action>? captureMetrics = null, CancellationTokenSource? shutdownSource = null, bool disableLoopTask = false) + { + _options = options; + _captureMetrics = captureMetrics ?? (_ => { }); + _shutdownSource = shutdownSource ?? new CancellationTokenSource(); + + if (disableLoopTask) + { + // We can stop the loop from running during testing + _options.LogDebug("LoopTask disabled."); + LoopTask = Task.CompletedTask; + } + else + { + options.LogDebug("Starting MetricsAggregator."); + LoopTask = Task.Run(RunLoopAsync); + } + } private static readonly DateTime EpochStart = new(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); internal static long GetTimeBucketKey(DateTime timestamp) @@ -114,11 +128,11 @@ private void Emit( Func addValuesFactory = type switch { - MetricType.Counter => (string _) => new CounterMetric(key, value, unit.Value, tags), - MetricType.Gauge => (string _) => new GaugeMetric(key, value, unit.Value, tags), - MetricType.Distribution => (string _) => new DistributionMetric(key, value, unit.Value, tags), - MetricType.Set => (string _) => new SetMetric(key, (int)value, unit.Value, tags), - _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + MetricType.Counter => (string _) => new CounterMetric(key, value, unit.Value, tags, timestamp), + MetricType.Gauge => (string _) => new GaugeMetric(key, value, unit.Value, tags, timestamp), + MetricType.Distribution => (string _) => new DistributionMetric(key, value, unit.Value, tags, timestamp), + MetricType.Set => (string _) => new SetMetric(key, (int)value, unit.Value, tags, timestamp), + _ => throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown MetricType") }; timeBucket.AddOrUpdate( @@ -132,29 +146,148 @@ private void Emit( ); } - // private void FlushData(object? state) - // { - // var metricsToFlush = new List(); - // - // foreach (var metric in _metrics) - // { - // metricsToFlush.Add(metric.Value); - // // Optionally, reset or remove the metric from _metrics - // } - // - // _onFlush(metricsToFlush); - // } - // - // // Method to force flush the data - // public void ForceFlush() - // { - // FlushData(null); - // } - // - // // Dispose pattern to clean up resources - // public void Dispose() - // { - // _flushTimer?.Dispose(); - // } + private async Task RunLoopAsync() + { + _options.LogDebug("MetricsAggregator Started."); + + using var shutdownTimeout = new CancellationTokenSource(); + var shutdownRequested = false; + + try + { + while (!shutdownTimeout.IsCancellationRequested) + { + // If the cancellation was signaled, run until the end of the queue or shutdownTimeout + if (!shutdownRequested) + { + try + { + var delay = Task.Delay(_flushInterval, shutdownTimeout.Token).ConfigureAwait(false); + await Task.Delay(_flushInterval, shutdownTimeout.Token).ConfigureAwait(false); + } + // Cancellation requested and no timeout allowed, so exit even if there are more items + catch (OperationCanceledException) when (_options.ShutdownTimeout == TimeSpan.Zero) + { + _options.LogDebug("Exiting immediately due to 0 shutdown timeout."); + + shutdownTimeout.Cancel(); + return; + } + // Cancellation requested, scheduled shutdown + catch (OperationCanceledException) + { + _options.LogDebug( + "Shutdown scheduled. Stopping by: {0}.", + _options.ShutdownTimeout); + + shutdownTimeout.CancelAfterSafe(_options.ShutdownTimeout); + + shutdownRequested = true; + } + } + + // Work with the envelope while it's in the queue + foreach (var key in GetFlushableBuckets()) + { + try + { + _options.LogDebug("Flushing metrics for bucket {0}", key); + var bucket = Buckets[key]; + _captureMetrics(bucket.Values); + } + catch (OperationCanceledException) + { + _options.LogInfo("Shutdown token triggered. Time to exit."); + return; + } + catch (Exception exception) + { + _options.LogError(exception, "Error while processing metric aggregates."); + } + finally + { + _options.LogDebug("Metric flushed for bucket {0}", key); + Buckets.TryRemove(key, out _); + } + } + } + } + catch (Exception e) + { + _options.LogFatal(e, "Exception in the Metric Aggregator."); + throw; + } + } + + /// + /// The aggregator shifts it's flushing by up to an entire rollup window to avoid multiple clients trampling on end + /// of a 10 second window as all the buckets are anchored to multiples of ROLLUP seconds. We randomize this number + /// once per aggregator boot to achieve some level of offsetting across a fleet of deployed SDKs. Relay itself will + /// also apply independent jittering. + /// + private readonly double _flushShift = new Random().NextDouble() * RollupInSeconds; + + /// + /// Returns the keys for any buckets that are ready to be flushed (i.e. are for periods before the cutoff) + /// + /// + /// An enumerable containing the keys for any buckets that are ready to be flushed + /// + internal IEnumerable GetFlushableBuckets() + { + var cutoff = DateTime.UtcNow + .Subtract(TimeSpan.FromSeconds(RollupInSeconds)) + .Subtract(TimeSpan.FromSeconds(_flushShift)); + + foreach (var bucket in Buckets) + { + var bucketTime = DateTimeOffset.FromUnixTimeSeconds(bucket.Key); + if (bucketTime < cutoff) + { + yield return bucket.Key; + } + } + } + + /// + /// Stops the background worker and waits for it to empty the queue until 'shutdownTimeout' is reached + /// + /// + public void Dispose() + { + _options.LogDebug("Disposing MetricAggregator."); + + if (_disposed) + { + _options.LogDebug("Already disposed MetricAggregator."); + return; + } + + _disposed = true; + + try + { + // Request the LoopTask stop. + _shutdownSource.Cancel(); + + // Now wait for the Loop to stop. + // NOTE: While non-intuitive, do not pass a timeout or cancellation token here. We are waiting for + // the _continuation_ of the method, not its _execution_. If we stop waiting prematurely, this may cause + // unexpected behavior in client applications. + LoopTask.Wait(); + } + catch (OperationCanceledException) + { + _options.LogDebug("Stopping the Metric Aggregator due to a cancellation."); + } + catch (Exception exception) + { + _options.LogError(exception, "Stopping the Metric Aggregator threw an exception."); + } + finally + { + _shutdownSource.Dispose(); + } + } } diff --git a/src/Sentry/Protocol/Envelopes/Envelope.cs b/src/Sentry/Protocol/Envelopes/Envelope.cs index 9b6bf1b6c6..17cc26bf64 100644 --- a/src/Sentry/Protocol/Envelopes/Envelope.cs +++ b/src/Sentry/Protocol/Envelopes/Envelope.cs @@ -322,16 +322,13 @@ public static Envelope FromTransaction(Transaction transaction) } /// - /// Creates an envelope that contains a + /// Creates an envelope that contains one or more Metrics /// - internal static Envelope FromMetric(Metric metric) + internal static Envelope FromMetrics(IEnumerable metrics) { var header = DefaultHeader; - var items = new[] - { - EnvelopeItem.FromMetric(metric) - }; + var items = metrics.Select(EnvelopeItem.FromMetric).ToArray(); return new Envelope(header, items); } diff --git a/src/Sentry/Protocol/Metrics/CounterMetric.cs b/src/Sentry/Protocol/Metrics/CounterMetric.cs index 6566d73ea3..30c3e0edf9 100644 --- a/src/Sentry/Protocol/Metrics/CounterMetric.cs +++ b/src/Sentry/Protocol/Metrics/CounterMetric.cs @@ -12,17 +12,17 @@ public CounterMetric() Value = 0; } - /// - /// Counters track a value that can only be incremented. - /// - public CounterMetric(string key, double value, MeasurementUnit? unit = null, IDictionary? tags = null) - : base(key, unit, tags) + public CounterMetric(string key, double value, MeasurementUnit? unit = null, IDictionary? tags = null, DateTime? timestamp = null) + : base(key, unit, tags, timestamp) { Value = value; } public double Value { get; private set; } + protected override string MetricType => "c"; + public override void Add(double value) => Value += value; protected override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) => diff --git a/src/Sentry/Protocol/Metrics/DistributionMetric.cs b/src/Sentry/Protocol/Metrics/DistributionMetric.cs index 6203a1b7a7..ba338176b8 100644 --- a/src/Sentry/Protocol/Metrics/DistributionMetric.cs +++ b/src/Sentry/Protocol/Metrics/DistributionMetric.cs @@ -14,14 +14,16 @@ public DistributionMetric() } public DistributionMetric(string key, double value, MeasurementUnit? unit = null, - IDictionary? tags = null) - : base(key, unit, tags) + IDictionary? tags = null, DateTime? timestamp = null) + : base(key, unit, tags, timestamp) { Value = new List() { value }; } public IList Value { get; set; } + protected override string MetricType => "d"; + public override void Add(double value) { Value.Add(value); diff --git a/src/Sentry/Protocol/Metrics/GaugeMetric.cs b/src/Sentry/Protocol/Metrics/GaugeMetric.cs index fd231c152f..617e20f7c7 100644 --- a/src/Sentry/Protocol/Metrics/GaugeMetric.cs +++ b/src/Sentry/Protocol/Metrics/GaugeMetric.cs @@ -17,8 +17,9 @@ public GaugeMetric() Count = 0; } - public GaugeMetric(string key, double value, MeasurementUnit? unit = null, IDictionary? tags = null) - : base(key, unit, tags) + public GaugeMetric(string key, double value, MeasurementUnit? unit = null, IDictionary? tags = null, + DateTime? timestamp = null) + : base(key, unit, tags, timestamp) { Value = value; First = value; @@ -35,6 +36,8 @@ public GaugeMetric(string key, double value, MeasurementUnit? unit = null, IDict public double Sum { get; private set; } public double Count { get; private set; } + protected override string MetricType => "g"; + public override void Add(double value) { Value = value; diff --git a/src/Sentry/Protocol/Metrics/Metric.cs b/src/Sentry/Protocol/Metrics/Metric.cs index 4c4e158d7d..073db5fa5f 100644 --- a/src/Sentry/Protocol/Metrics/Metric.cs +++ b/src/Sentry/Protocol/Metrics/Metric.cs @@ -9,19 +9,24 @@ protected Metric() : this(string.Empty) { } - protected Metric(string key, MeasurementUnit? unit = null, IDictionary? tags = null) + protected Metric(string key, MeasurementUnit? unit = null, IDictionary? tags = null, DateTime? timestamp = null) { Key = key; Unit = unit; Tags = tags ?? new Dictionary(); + Timestamp = timestamp ?? DateTime.UtcNow; } public string Key { get; private set; } + public DateTime Timestamp { get; private set; } + public MeasurementUnit? Unit { get; private set; } public IDictionary Tags { get; private set; } + protected abstract string MetricType { get; } + public abstract void Add(double value); protected abstract void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger); @@ -29,7 +34,9 @@ protected Metric(string key, MeasurementUnit? unit = null, IDictionary(); } - public SetMetric(string key, int value, MeasurementUnit? unit = null, IDictionary? tags = null) - : base(key, unit, tags) + public SetMetric(string key, int value, MeasurementUnit? unit = null, IDictionary? tags = null, + DateTime? timestamp = null) + : base(key, unit, tags, timestamp) { Value = new HashSet() { value }; } public HashSet Value { get; private set; } + protected override string MetricType => "s"; + public override void Add(double value) { Value.Add((int)value); diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index fda6de8ba5..e51376e75a 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -77,7 +77,7 @@ internal SentryClient( if (options.ExperimentalMetrics is { MetricSampleRate: > 0 } experimentalMetricsOptions) { - Metrics = new MetricAggregator(); + Metrics = new MetricAggregator(options, CaptureMetrics); } else { @@ -238,10 +238,12 @@ public void CaptureTransaction(Transaction transaction, Scope? scope, Hint? hint return transaction; } - /// - internal void CaptureMetric(Metric metric) + /// + /// Captures one or more metrics to be sent to Sentry. + /// + internal void CaptureMetrics(IEnumerable metrics) { - CaptureEnvelope(Envelope.FromMetric(metric)); + CaptureEnvelope(Envelope.FromMetrics(metrics)); } /// diff --git a/test/Sentry.Tests/MetricAggregatorTests.cs b/test/Sentry.Tests/MetricAggregatorTests.cs index d35ab33bd8..c60298b2f1 100644 --- a/test/Sentry.Tests/MetricAggregatorTests.cs +++ b/test/Sentry.Tests/MetricAggregatorTests.cs @@ -6,8 +6,11 @@ public class MetricAggregatorTests { class Fixture { + public SentryOptions Options { get; set; } = new(); + public Action> CaptureMetrics { get; set; } = (_ => { }); + public bool DisableFlushLoop { get; set; } = true; public MetricAggregator GetSut() - => new(); + => new(Options, CaptureMetrics, disableLoopTask: DisableFlushLoop); } private readonly Fixture _fixture = new(); @@ -54,7 +57,7 @@ public void Increment_AggregatesMetrics() var key = "counter_key"; var unit = MeasurementUnit.None; var tags = new Dictionary { ["tag1"] = "value1" }; - var sut = new MetricAggregator(); + var sut = _fixture.GetSut(); // Act DateTime firstTime = new(1970, 1, 1, 0, 0, 31, 0, DateTimeKind.Utc); @@ -84,7 +87,7 @@ public void Gauge_AggregatesMetrics() var key = "gauge_key"; var unit = MeasurementUnit.None; var tags = new Dictionary { ["tag1"] = "value1" }; - var sut = new MetricAggregator(); + var sut = _fixture.GetSut(); // Act DateTime time1 = new(1970, 1, 1, 0, 0, 31, 0, DateTimeKind.Utc); @@ -124,7 +127,7 @@ public void Distribution_AggregatesMetrics() var key = "distribution_key"; var unit = MeasurementUnit.None; var tags = new Dictionary { ["tag1"] = "value1" }; - var sut = new MetricAggregator(); + var sut = _fixture.GetSut(); // Act DateTime time1 = new(1970, 1, 1, 0, 0, 31, 0, DateTimeKind.Utc); @@ -154,7 +157,7 @@ public void Set_AggregatesMetrics() var key = "set_key"; var unit = MeasurementUnit.None; var tags = new Dictionary { ["tag1"] = "value1" }; - var sut = new MetricAggregator(); + var sut = _fixture.GetSut(); // Act DateTime time1 = new(1970, 1, 1, 0, 0, 31, 0, DateTimeKind.Utc); From c83e9c91840d0f78b070df426d318b43493f0cca Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 7 Dec 2023 22:09:43 +1300 Subject: [PATCH 10/52] Implemented statsd serialization --- .../Program.cs | 8 +- src/Sentry/MetricAggregator.cs | 52 ++++++----- src/Sentry/Protocol/Envelopes/EnvelopeItem.cs | 3 +- src/Sentry/Protocol/Metrics/CounterMetric.cs | 9 +- .../Protocol/Metrics/DistributionMetric.cs | 7 +- src/Sentry/Protocol/Metrics/GaugeMetric.cs | 13 ++- src/Sentry/Protocol/Metrics/Metric.cs | 90 +++++++++++++++++-- src/Sentry/Protocol/Metrics/SetMetric.cs | 7 +- src/Sentry/SentryClient.cs | 1 + test/Sentry.Tests/MetricAggregatorTests.cs | 18 ++-- 10 files changed, 155 insertions(+), 53 deletions(-) diff --git a/samples/Sentry.Samples.AspNetCore.Basic/Program.cs b/samples/Sentry.Samples.AspNetCore.Basic/Program.cs index d982123b56..5bfdd48f60 100644 --- a/samples/Sentry.Samples.AspNetCore.Basic/Program.cs +++ b/samples/Sentry.Samples.AspNetCore.Basic/Program.cs @@ -15,11 +15,12 @@ public static IWebHost BuildWebHost(string[] args) => .UseSentry(o => { // A DSN is required. You can set it here, or in configuration, or in an environment variable. - o.Dsn = "https://eb18e953812b41c3aeb042e666fd3b5c@o447951.ingest.sentry.io/5428537"; + o.Dsn = "https://b887218a80114d26a9b1a51c5f88e0b4@o447951.ingest.sentry.io/6601807"; // Enable Sentry performance monitoring o.EnableTracing = true; + o.ExperimentalMetrics = new ExperimentalMetricsOptions(){ MetricSampleRate = 1.0 }; #if DEBUG // Log debug information about the Sentry SDK o.Debug = true; @@ -35,6 +36,11 @@ public static IWebHost BuildWebHost(string[] args) => // exception when serving a request to path: /throw app.UseEndpoints(endpoints => { + endpoints.MapGet("/hello", () => + { + SentrySdk.Metrics.Increment("hello.world"); + return "Hello World!"; + }); // Reported events will be grouped by route pattern endpoints.MapGet("/throw/{message?}", context => { diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index 966b0c732d..47a980b45c 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -8,8 +8,6 @@ internal class MetricAggregator : IMetricAggregator, IDisposable { internal enum MetricType : byte { Counter, Gauge, Distribution, Set } - private const int RollupInSeconds = 10; - private readonly SentryOptions _options; private readonly Action> _captureMetrics; private readonly TimeSpan _flushInterval = TimeSpan.FromSeconds(5); @@ -45,15 +43,6 @@ public MetricAggregator(SentryOptions options, Action>? capt } } - private static readonly DateTime EpochStart = new(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); - internal static long GetTimeBucketKey(DateTime timestamp) - { - var seconds = (long)(timestamp.ToUniversalTime() - EpochStart).TotalSeconds; - - // var seconds = (timestamp?.ToUniversalTime() ?? DateTimeOffset.UtcNow).ToUnixTimeSeconds(); - return (seconds / RollupInSeconds) * RollupInSeconds; - } - internal static string GetMetricBucketKey(MetricType type, string metricKey, MeasurementUnit unit, IDictionary? tags) { var typePrefix = type switch @@ -122,7 +111,7 @@ private void Emit( timestamp ??= DateTime.UtcNow; unit ??= MeasurementUnit.None; var timeBucket = Buckets.GetOrAdd( - GetTimeBucketKey(timestamp.Value), + timestamp.Value.GetTimeBucketKey(), _ => new ConcurrentDictionary() ); @@ -190,6 +179,7 @@ private async Task RunLoopAsync() // Work with the envelope while it's in the queue foreach (var key in GetFlushableBuckets()) { + // TODO: Check if a shutdown request has been made try { _options.LogDebug("Flushing metrics for bucket {0}", key); @@ -220,14 +210,6 @@ private async Task RunLoopAsync() } } - /// - /// The aggregator shifts it's flushing by up to an entire rollup window to avoid multiple clients trampling on end - /// of a 10 second window as all the buckets are anchored to multiples of ROLLUP seconds. We randomize this number - /// once per aggregator boot to achieve some level of offsetting across a fleet of deployed SDKs. Relay itself will - /// also apply independent jittering. - /// - private readonly double _flushShift = new Random().NextDouble() * RollupInSeconds; - /// /// Returns the keys for any buckets that are ready to be flushed (i.e. are for periods before the cutoff) /// @@ -236,10 +218,7 @@ private async Task RunLoopAsync() /// internal IEnumerable GetFlushableBuckets() { - var cutoff = DateTime.UtcNow - .Subtract(TimeSpan.FromSeconds(RollupInSeconds)) - .Subtract(TimeSpan.FromSeconds(_flushShift)); - + var cutoff = MetricBucketHelper.GetCutoff(); foreach (var bucket in Buckets) { var bucketTime = DateTimeOffset.FromUnixTimeSeconds(bucket.Key); @@ -288,6 +267,31 @@ public void Dispose() finally { _shutdownSource.Dispose(); + LoopTask.Dispose(); } } } + +internal static class MetricBucketHelper +{ + private const int RollupInSeconds = 10; + + private static readonly DateTime EpochStart = new(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + + internal static long GetTimeBucketKey(this DateTime timestamp) + { + var seconds = (long)(timestamp.ToUniversalTime() - EpochStart).TotalSeconds; + return (seconds / RollupInSeconds) * RollupInSeconds; + } + + /// + /// The aggregator shifts it's flushing by up to an entire rollup window to avoid multiple clients trampling on end + /// of a 10 second window as all the buckets are anchored to multiples of ROLLUP seconds. We randomize this number + /// once per aggregator boot to achieve some level of offsetting across a fleet of deployed SDKs. Relay itself will + /// also apply independent jittering. + /// + private static readonly double _flushShift = new Random().NextDouble() * RollupInSeconds; + internal static DateTime GetCutoff() => DateTime.UtcNow + .Subtract(TimeSpan.FromSeconds(RollupInSeconds)) + .Subtract(TimeSpan.FromSeconds(_flushShift)); +} diff --git a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs index 13020583db..5addcbf89c 100644 --- a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs +++ b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs @@ -233,7 +233,8 @@ internal static EnvelopeItem FromMetric(Metric metric) [TypeKey] = TypeValueMetric }; - return new EnvelopeItem(header, new JsonSerializable(metric)); + // Note that metrics are serialized using statsd encoding (not JSON) + return new EnvelopeItem(header, metric); } /// diff --git a/src/Sentry/Protocol/Metrics/CounterMetric.cs b/src/Sentry/Protocol/Metrics/CounterMetric.cs index 30c3e0edf9..ffaffe4fad 100644 --- a/src/Sentry/Protocol/Metrics/CounterMetric.cs +++ b/src/Sentry/Protocol/Metrics/CounterMetric.cs @@ -21,10 +21,13 @@ public CounterMetric(string key, double value, MeasurementUnit? unit = null, IDi public double Value { get; private set; } - protected override string MetricType => "c"; - public override void Add(double value) => Value += value; - protected override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) => + protected override void WriteValues(Utf8JsonWriter writer, IDiagnosticLogger? logger) => writer.WriteNumber("value", Value); + + protected override IEnumerable SerializedStatsdValues() + { + yield return Value; + } } diff --git a/src/Sentry/Protocol/Metrics/DistributionMetric.cs b/src/Sentry/Protocol/Metrics/DistributionMetric.cs index ba338176b8..42235157f1 100644 --- a/src/Sentry/Protocol/Metrics/DistributionMetric.cs +++ b/src/Sentry/Protocol/Metrics/DistributionMetric.cs @@ -22,13 +22,14 @@ public DistributionMetric(string key, double value, MeasurementUnit? unit = null public IList Value { get; set; } - protected override string MetricType => "d"; - public override void Add(double value) { Value.Add(value); } - protected override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) => + protected override void WriteValues(Utf8JsonWriter writer, IDiagnosticLogger? logger) => writer.WriteArrayIfNotEmpty("value", Value, logger); + + protected override IEnumerable SerializedStatsdValues() + => Value.Select(v => (IConvertible)v); } diff --git a/src/Sentry/Protocol/Metrics/GaugeMetric.cs b/src/Sentry/Protocol/Metrics/GaugeMetric.cs index 617e20f7c7..1bd48d345b 100644 --- a/src/Sentry/Protocol/Metrics/GaugeMetric.cs +++ b/src/Sentry/Protocol/Metrics/GaugeMetric.cs @@ -36,8 +36,6 @@ public GaugeMetric(string key, double value, MeasurementUnit? unit = null, IDict public double Sum { get; private set; } public double Count { get; private set; } - protected override string MetricType => "g"; - public override void Add(double value) { Value = value; @@ -47,7 +45,7 @@ public override void Add(double value) Count++; } - protected override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) + protected override void WriteValues(Utf8JsonWriter writer, IDiagnosticLogger? logger) { writer.WriteNumber("value", Value); writer.WriteNumber("first", First); @@ -56,4 +54,13 @@ protected override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnost writer.WriteNumber("sum", Sum); writer.WriteNumber("count", Count); } + + protected override IEnumerable SerializedStatsdValues() + { + yield return Value; + yield return Min; + yield return Max; + yield return Sum; + yield return Count; + } } diff --git a/src/Sentry/Protocol/Metrics/Metric.cs b/src/Sentry/Protocol/Metrics/Metric.cs index 073db5fa5f..0f4efb6204 100644 --- a/src/Sentry/Protocol/Metrics/Metric.cs +++ b/src/Sentry/Protocol/Metrics/Metric.cs @@ -1,9 +1,10 @@ using Sentry.Extensibility; using Sentry.Internal.Extensions; +using ISentrySerializable = Sentry.Protocol.Envelopes.ISerializable; namespace Sentry.Protocol.Metrics; -internal abstract class Metric : IJsonSerializable +internal abstract class Metric : IJsonSerializable, ISentrySerializable { protected Metric() : this(string.Empty) { @@ -17,6 +18,8 @@ protected Metric(string key, MeasurementUnit? unit = null, IDictionary Tags { get; private set; } - protected abstract string MetricType { get; } - public abstract void Add(double value); - protected abstract void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger); + protected abstract void WriteValues(Utf8JsonWriter writer, IDiagnosticLogger? logger); public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) { writer.WriteStartObject(); - writer.WriteString("type", MetricType); + writer.WriteString("type", GetType().Name); + writer.WriteSerializable("event_id", EventId, logger); writer.WriteString("name", Key); writer.WriteString("timestamp", Timestamp); if (Unit.HasValue) @@ -42,7 +44,83 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) writer.WriteStringIfNotWhiteSpace("unit", Unit.ToString()); } writer.WriteStringDictionaryIfNotEmpty("tags", Tags!); - WriteConcreteProperties(writer, logger); + WriteValues(writer, logger); writer.WriteEndObject(); } + + protected abstract IEnumerable SerializedStatsdValues(); + + internal static string SanitizeKey(string input) => Regex.Replace(input, @"[^a-zA-Z0-9_/.-]+", "_"); + internal static string SanitizeValue(string input) => Regex.Replace(input, @"[^\w\d_:/@\.\{\}\[\]$-]+", "_"); + + public async Task SerializeAsync(Stream stream, IDiagnosticLogger? logger, CancellationToken cancellationToken = default) + { + /* + * We're serializing using the statsd format here: https://github.com/b/statsd_spec + */ + var metricName = SanitizeKey(Key); + await Write(metricName).ConfigureAwait(false); + await Write("@").ConfigureAwait(false); + var unit = Unit ?? MeasurementUnit.None; + await Write(unit.ToString()).ConfigureAwait(false); + + foreach (var value in SerializedStatsdValues()) + { + await Write(":").ConfigureAwait(false); + await Write(value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); + } + + await Write("|").ConfigureAwait(false); + await Write(StatsdType).ConfigureAwait(false); + + if (Tags is { Count: > 0 } tags) + { + await Write("|#").ConfigureAwait(false); + var first = true; + foreach (var (key, value) in tags) + { + var tagKey = SanitizeKey(key); + if (string.IsNullOrWhiteSpace(tagKey)) + { + continue; + } + if (first) + { + first = false; + } + else + { + await Write(",").ConfigureAwait(false); + } + await Write(key).ConfigureAwait(false); + await Write(":").ConfigureAwait(false); + await Write(SanitizeValue(value)).ConfigureAwait(false); + } + } + + await Write("|T").ConfigureAwait(false); + await Write(Timestamp.GetTimeBucketKey().ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); + await Write("\n").ConfigureAwait(false); + return; + + async Task Write(string content) + { + await stream.WriteAsync(Encoding.UTF8.GetBytes(content), cancellationToken).ConfigureAwait(false); + } + } + + public void Serialize(Stream stream, IDiagnosticLogger? logger) + { + SerializeAsync(stream, logger).GetAwaiter().GetResult(); + } + + private string StatsdType => + this switch + { + CounterMetric _ => "c", + GaugeMetric _ => "g", + DistributionMetric _ => "d", + SetMetric _ => "s", + _ => throw new ArgumentOutOfRangeException(GetType().Name, "Unable to infer statsd type") + }; } diff --git a/src/Sentry/Protocol/Metrics/SetMetric.cs b/src/Sentry/Protocol/Metrics/SetMetric.cs index 51fdb740d8..d26ed6716f 100644 --- a/src/Sentry/Protocol/Metrics/SetMetric.cs +++ b/src/Sentry/Protocol/Metrics/SetMetric.cs @@ -22,13 +22,14 @@ public SetMetric(string key, int value, MeasurementUnit? unit = null, IDictionar public HashSet Value { get; private set; } - protected override string MetricType => "s"; - public override void Add(double value) { Value.Add((int)value); } - protected override void WriteConcreteProperties(Utf8JsonWriter writer, IDiagnosticLogger? logger) => + protected override void WriteValues(Utf8JsonWriter writer, IDiagnosticLogger? logger) => writer.WriteArrayIfNotEmpty("value", Value, logger); + + protected override IEnumerable SerializedStatsdValues() + => Value.Select(v => (IConvertible)v); } diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index e51376e75a..d9c2486bbd 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -243,6 +243,7 @@ public void CaptureTransaction(Transaction transaction, Scope? scope, Hint? hint /// internal void CaptureMetrics(IEnumerable metrics) { + _options.LogDebug($"Capturing metrics"); CaptureEnvelope(Envelope.FromMetrics(metrics)); } diff --git a/test/Sentry.Tests/MetricAggregatorTests.cs b/test/Sentry.Tests/MetricAggregatorTests.cs index c60298b2f1..39f676a5c2 100644 --- a/test/Sentry.Tests/MetricAggregatorTests.cs +++ b/test/Sentry.Tests/MetricAggregatorTests.cs @@ -27,7 +27,7 @@ public void GetTimeBucketKey_RoundsDownToNearestTenSeconds(int seconds) var timestamp = new DateTime(1970, 1, 1, 1, 1, seconds, DateTimeKind.Utc); // Act - var result = MetricAggregator.GetTimeBucketKey(timestamp); + var result = timestamp.GetTimeBucketKey(); // Assert result.Should().Be(3690); // (1 hour) + (1 minute) plus (30 seconds) = 3690 @@ -70,11 +70,11 @@ public void Increment_AggregatesMetrics() sut.Increment(key, 13, unit, tags, thirdTime); // Assert - var bucket1 = sut.Buckets[MetricAggregator.GetTimeBucketKey(firstTime)]; + var bucket1 = sut.Buckets[firstTime.GetTimeBucketKey()]; var data1 = (CounterMetric)bucket1[MetricAggregator.GetMetricBucketKey(metricType, key, unit, tags)]; data1.Value.Should().Be(8); // First two emits are in the same bucket - var bucket2 = sut.Buckets[MetricAggregator.GetTimeBucketKey(thirdTime)]; + var bucket2 = sut.Buckets[thirdTime.GetTimeBucketKey()]; var data2 = (CounterMetric)bucket2[MetricAggregator.GetMetricBucketKey(metricType, key, unit, tags)]; data2.Value.Should().Be(13); // First two emits are in the same bucket } @@ -100,7 +100,7 @@ public void Gauge_AggregatesMetrics() sut.Gauge(key, 13, unit, tags, time3); // Assert - var bucket1 = sut.Buckets[MetricAggregator.GetTimeBucketKey(time1)]; + var bucket1 = sut.Buckets[time1.GetTimeBucketKey()]; var data1 = (GaugeMetric)bucket1[MetricAggregator.GetMetricBucketKey(metricType, key, unit, tags)]; data1.Value.Should().Be(5); data1.First.Should().Be(3); @@ -109,7 +109,7 @@ public void Gauge_AggregatesMetrics() data1.Sum.Should().Be(8); data1.Count.Should().Be(2); - var bucket2 = sut.Buckets[MetricAggregator.GetTimeBucketKey(time3)]; + var bucket2 = sut.Buckets[time3.GetTimeBucketKey()]; var data2 = (GaugeMetric)bucket2[MetricAggregator.GetMetricBucketKey(metricType, key, unit, tags)]; data2.Value.Should().Be(13); data2.First.Should().Be(13); @@ -140,11 +140,11 @@ public void Distribution_AggregatesMetrics() sut.Distribution(key, 13, unit, tags, time3); // Assert - var bucket1 = sut.Buckets[MetricAggregator.GetTimeBucketKey(time1)]; + var bucket1 = sut.Buckets[time1.GetTimeBucketKey()]; var data1 = (DistributionMetric)bucket1[MetricAggregator.GetMetricBucketKey(metricType, key, unit, tags)]; data1.Value.Should().BeEquivalentTo(new[] {3, 5}); - var bucket2 = sut.Buckets[MetricAggregator.GetTimeBucketKey(time3)]; + var bucket2 = sut.Buckets[time3.GetTimeBucketKey()]; var data2 = (DistributionMetric)bucket2[MetricAggregator.GetMetricBucketKey(metricType, key, unit, tags)]; data2.Value.Should().BeEquivalentTo(new[] {13}); } @@ -173,11 +173,11 @@ public void Set_AggregatesMetrics() sut.Set(key, 13, unit, tags, time3); // Assert - var bucket1 = sut.Buckets[MetricAggregator.GetTimeBucketKey(time1)]; + var bucket1 = sut.Buckets[time1.GetTimeBucketKey()]; var data1 = (SetMetric)bucket1[MetricAggregator.GetMetricBucketKey(metricType, key, unit, tags)]; data1.Value.Should().BeEquivalentTo(new[] {3, 5}); - var bucket2 = sut.Buckets[MetricAggregator.GetTimeBucketKey(time3)]; + var bucket2 = sut.Buckets[time3.GetTimeBucketKey()]; var data2 = (SetMetric)bucket2[MetricAggregator.GetMetricBucketKey(metricType, key, unit, tags)]; data2.Value.Should().BeEquivalentTo(new[] {13}); } From 91500984331e5a74913127dafd9c2c668917127b Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 7 Dec 2023 22:24:45 +1300 Subject: [PATCH 11/52] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f0ef6666f..469bee6e50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Experimental pre-release availability of Delightful Developer Metrics. We're exploring the use of Metrics in Sentry. The API will very likely change and we don't yet have any documentation. ([#2949](https://github.com/getsentry/sentry-dotnet/pull/2949)) + ### Dependencies - Bump Cocoa SDK from v8.17.0 to v8.17.1 ([#2936](https://github.com/getsentry/sentry-dotnet/pull/2936)) From 19dc74aa20d4d76fe6c94b3f78289b35517fe6af Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 7 Dec 2023 22:38:07 +1300 Subject: [PATCH 12/52] Split tests for Aggregagtor and BucketHelper --- src/Sentry/MetricAggregator.cs | 24 ------------------- src/Sentry/MetricBucketHelper.cs | 25 ++++++++++++++++++++ test/Sentry.Tests/MetricAggregatorTests.cs | 18 -------------- test/Sentry.Tests/MetricBucketHelperTests.cs | 24 +++++++++++++++++++ 4 files changed, 49 insertions(+), 42 deletions(-) create mode 100644 src/Sentry/MetricBucketHelper.cs create mode 100644 test/Sentry.Tests/MetricBucketHelperTests.cs diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index 47a980b45c..0c36d97b06 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -271,27 +271,3 @@ public void Dispose() } } } - -internal static class MetricBucketHelper -{ - private const int RollupInSeconds = 10; - - private static readonly DateTime EpochStart = new(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); - - internal static long GetTimeBucketKey(this DateTime timestamp) - { - var seconds = (long)(timestamp.ToUniversalTime() - EpochStart).TotalSeconds; - return (seconds / RollupInSeconds) * RollupInSeconds; - } - - /// - /// The aggregator shifts it's flushing by up to an entire rollup window to avoid multiple clients trampling on end - /// of a 10 second window as all the buckets are anchored to multiples of ROLLUP seconds. We randomize this number - /// once per aggregator boot to achieve some level of offsetting across a fleet of deployed SDKs. Relay itself will - /// also apply independent jittering. - /// - private static readonly double _flushShift = new Random().NextDouble() * RollupInSeconds; - internal static DateTime GetCutoff() => DateTime.UtcNow - .Subtract(TimeSpan.FromSeconds(RollupInSeconds)) - .Subtract(TimeSpan.FromSeconds(_flushShift)); -} diff --git a/src/Sentry/MetricBucketHelper.cs b/src/Sentry/MetricBucketHelper.cs new file mode 100644 index 0000000000..03c4af374a --- /dev/null +++ b/src/Sentry/MetricBucketHelper.cs @@ -0,0 +1,25 @@ +namespace Sentry; + +internal static class MetricBucketHelper +{ + private const int RollupInSeconds = 10; + + private static readonly DateTime EpochStart = new(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + + internal static long GetTimeBucketKey(this DateTime timestamp) + { + var seconds = (long)(timestamp.ToUniversalTime() - EpochStart).TotalSeconds; + return (seconds / RollupInSeconds) * RollupInSeconds; + } + + /// + /// The aggregator shifts it's flushing by up to an entire rollup window to avoid multiple clients trampling on end + /// of a 10 second window as all the buckets are anchored to multiples of ROLLUP seconds. We randomize this number + /// once per aggregator boot to achieve some level of offsetting across a fleet of deployed SDKs. Relay itself will + /// also apply independent jittering. + /// + private static readonly double _flushShift = new Random().NextDouble() * RollupInSeconds; + internal static DateTime GetCutoff() => DateTime.UtcNow + .Subtract(TimeSpan.FromSeconds(RollupInSeconds)) + .Subtract(TimeSpan.FromSeconds(_flushShift)); +} diff --git a/test/Sentry.Tests/MetricAggregatorTests.cs b/test/Sentry.Tests/MetricAggregatorTests.cs index 39f676a5c2..1a9d509a7f 100644 --- a/test/Sentry.Tests/MetricAggregatorTests.cs +++ b/test/Sentry.Tests/MetricAggregatorTests.cs @@ -15,24 +15,6 @@ public MetricAggregator GetSut() private readonly Fixture _fixture = new(); - [Theory] - [InlineData(30)] - [InlineData(31)] - [InlineData(39)] - public void GetTimeBucketKey_RoundsDownToNearestTenSeconds(int seconds) - { - // Arrange - // Returns the number of seconds that have elapsed since 1970-01-01T00:00:00Z - // var timestamp = new DateTime(2023, 1, 15, 17, 42, 31, DateTimeKind.Utc); - var timestamp = new DateTime(1970, 1, 1, 1, 1, seconds, DateTimeKind.Utc); - - // Act - var result = timestamp.GetTimeBucketKey(); - - // Assert - result.Should().Be(3690); // (1 hour) + (1 minute) plus (30 seconds) = 3690 - } - [Fact] public void GetMetricBucketKey_GeneratesExpectedKey() { diff --git a/test/Sentry.Tests/MetricBucketHelperTests.cs b/test/Sentry.Tests/MetricBucketHelperTests.cs new file mode 100644 index 0000000000..ccfb306a48 --- /dev/null +++ b/test/Sentry.Tests/MetricBucketHelperTests.cs @@ -0,0 +1,24 @@ +using Sentry.Protocol.Metrics; + +namespace Sentry.Tests; + +public class MetricAggregatorTests +{ + [Theory] + [InlineData(30)] + [InlineData(31)] + [InlineData(39)] + public void GetTimeBucketKey_RoundsDownToNearestTenSeconds(int seconds) + { + // Arrange + // Returns the number of seconds that have elapsed since 1970-01-01T00:00:00Z + // var timestamp = new DateTime(2023, 1, 15, 17, 42, 31, DateTimeKind.Utc); + var timestamp = new DateTime(1970, 1, 1, 1, 1, seconds, DateTimeKind.Utc); + + // Act + var result = timestamp.GetTimeBucketKey(); + + // Assert + result.Should().Be(3690); // (1 hour) + (1 minute) plus (30 seconds) = 3690 + } +} From cac0d79877250fe099c2cfe2448290378857aeeb Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 7 Dec 2023 23:06:58 +1300 Subject: [PATCH 13/52] Update MetricBucketHelperTests.cs --- test/Sentry.Tests/MetricBucketHelperTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Sentry.Tests/MetricBucketHelperTests.cs b/test/Sentry.Tests/MetricBucketHelperTests.cs index ccfb306a48..c49af2fc60 100644 --- a/test/Sentry.Tests/MetricBucketHelperTests.cs +++ b/test/Sentry.Tests/MetricBucketHelperTests.cs @@ -2,7 +2,7 @@ namespace Sentry.Tests; -public class MetricAggregatorTests +public class MetricBucketHelperTests { [Theory] [InlineData(30)] From 721e6b32534c2b15c2aec690a9e5b7ca6f475e60 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 11 Dec 2023 12:13:46 +1300 Subject: [PATCH 14/52] Integrated review feedback --- src/Sentry/IMetricAggregator.cs | 16 ++++++++++---- src/Sentry/MetricAggregator.cs | 23 ++++++++++++-------- src/Sentry/Protocol/Envelopes/Envelope.cs | 6 +++++- src/Sentry/Protocol/Metrics/Metric.cs | 26 ++++++++++------------- 4 files changed, 42 insertions(+), 29 deletions(-) diff --git a/src/Sentry/IMetricAggregator.cs b/src/Sentry/IMetricAggregator.cs index 754eeb9539..790d3ee5a5 100644 --- a/src/Sentry/IMetricAggregator.cs +++ b/src/Sentry/IMetricAggregator.cs @@ -13,7 +13,9 @@ public interface IMetricAggregator /// The value to be added /// An optional /// Optional Tags to associate with the metric - /// The time when the metric was emitted + /// + /// The time when the metric was emitted. Defaults to the time at which the metric is emitted, if no value is provided. + /// void Increment( string key, double value = 1.0, @@ -30,7 +32,9 @@ void Increment( /// The value to be added /// An optional /// Optional Tags to associate with the metric - /// The time when the metric was emitted + /// + /// The time when the metric was emitted. Defaults to the time at which the metric is emitted, if no value is provided. + /// void Gauge( string key, double value = 1.0, @@ -47,7 +51,9 @@ void Gauge( /// The value to be added /// An optional /// Optional Tags to associate with the metric - /// The time when the metric was emitted + /// + /// The time when the metric was emitted. Defaults to the time at which the metric is emitted, if no value is provided. + /// void Distribution( string key, double value = 1.0, @@ -64,7 +70,9 @@ void Distribution( /// The value to be added /// An optional /// Optional Tags to associate with the metric - /// The time when the metric was emitted + /// + /// The time when the metric was emitted. Defaults to the time at which the metric is emitted, if no value is provided. + /// void Set( string key, double value = 1.0, diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index 0c36d97b06..a9826ccedf 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -151,7 +151,6 @@ private async Task RunLoopAsync() { try { - var delay = Task.Delay(_flushInterval, shutdownTimeout.Token).ConfigureAwait(false); await Task.Delay(_flushInterval, shutdownTimeout.Token).ConfigureAwait(false); } // Cancellation requested and no timeout allowed, so exit even if there are more items @@ -179,12 +178,18 @@ private async Task RunLoopAsync() // Work with the envelope while it's in the queue foreach (var key in GetFlushableBuckets()) { - // TODO: Check if a shutdown request has been made + if (shutdownRequested) + { + break; + } try { _options.LogDebug("Flushing metrics for bucket {0}", key); - var bucket = Buckets[key]; - _captureMetrics(bucket.Values); + if (Buckets.TryRemove(key, out var bucket)) + { + _captureMetrics(bucket.Values); + _options.LogDebug("Metric flushed for bucket {0}", key); + } } catch (OperationCanceledException) { @@ -195,11 +200,6 @@ private async Task RunLoopAsync() { _options.LogError(exception, "Error while processing metric aggregates."); } - finally - { - _options.LogDebug("Metric flushed for bucket {0}", key); - Buckets.TryRemove(key, out _); - } } } } @@ -218,6 +218,11 @@ private async Task RunLoopAsync() /// internal IEnumerable GetFlushableBuckets() { + if (!_buckets.IsValueCreated) + { + yield break; + } + var cutoff = MetricBucketHelper.GetCutoff(); foreach (var bucket in Buckets) { diff --git a/src/Sentry/Protocol/Envelopes/Envelope.cs b/src/Sentry/Protocol/Envelopes/Envelope.cs index 17cc26bf64..60b292feee 100644 --- a/src/Sentry/Protocol/Envelopes/Envelope.cs +++ b/src/Sentry/Protocol/Envelopes/Envelope.cs @@ -328,7 +328,11 @@ internal static Envelope FromMetrics(IEnumerable metrics) { var header = DefaultHeader; - var items = metrics.Select(EnvelopeItem.FromMetric).ToArray(); + List items = new(); + foreach (var metric in metrics) + { + items.Add(EnvelopeItem.FromMetric(metric)); + } return new Envelope(header, items); } diff --git a/src/Sentry/Protocol/Metrics/Metric.cs b/src/Sentry/Protocol/Metrics/Metric.cs index 0f4efb6204..c896195f45 100644 --- a/src/Sentry/Protocol/Metrics/Metric.cs +++ b/src/Sentry/Protocol/Metrics/Metric.cs @@ -59,23 +59,22 @@ public async Task SerializeAsync(Stream stream, IDiagnosticLogger? logger, Cance * We're serializing using the statsd format here: https://github.com/b/statsd_spec */ var metricName = SanitizeKey(Key); - await Write(metricName).ConfigureAwait(false); - await Write("@").ConfigureAwait(false); + await Write($"{metricName}@").ConfigureAwait(false); var unit = Unit ?? MeasurementUnit.None; - await Write(unit.ToString()).ConfigureAwait(false); +// We don't need ConfigureAwait(false) here as ConfigureAwait on metricName above avoids capturing the ExecutionContext. +#pragma warning disable CA2007 + await Write(unit.ToString()); foreach (var value in SerializedStatsdValues()) { - await Write(":").ConfigureAwait(false); - await Write(value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); + await Write($":{value.ToString(CultureInfo.InvariantCulture)}"); } - await Write("|").ConfigureAwait(false); - await Write(StatsdType).ConfigureAwait(false); + await Write($"|{StatsdType}"); if (Tags is { Count: > 0 } tags) { - await Write("|#").ConfigureAwait(false); + await Write("|#"); var first = true; foreach (var (key, value) in tags) { @@ -90,17 +89,14 @@ public async Task SerializeAsync(Stream stream, IDiagnosticLogger? logger, Cance } else { - await Write(",").ConfigureAwait(false); + await Write(","); } - await Write(key).ConfigureAwait(false); - await Write(":").ConfigureAwait(false); - await Write(SanitizeValue(value)).ConfigureAwait(false); + await Write($"{key}:SanitizeValue(value)"); } } +#pragma warning restore CA2007 - await Write("|T").ConfigureAwait(false); - await Write(Timestamp.GetTimeBucketKey().ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); - await Write("\n").ConfigureAwait(false); + await Write($"|T{Timestamp.GetTimeBucketKey().ToString(CultureInfo.InvariantCulture)}\n").ConfigureAwait(false); return; async Task Write(string content) From a7ba47ee7d12b04d83ca2be5c4e25011c7efd2c4 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 11 Dec 2023 18:09:18 +1300 Subject: [PATCH 15/52] Create MetricTests.verify.cs --- test/Sentry.Tests/MetricTests.verify.cs | 49 +++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 test/Sentry.Tests/MetricTests.verify.cs diff --git a/test/Sentry.Tests/MetricTests.verify.cs b/test/Sentry.Tests/MetricTests.verify.cs new file mode 100644 index 0000000000..269ba37a54 --- /dev/null +++ b/test/Sentry.Tests/MetricTests.verify.cs @@ -0,0 +1,49 @@ +using Sentry.Protocol.Metrics; +using ISentrySerializable = Sentry.Protocol.Envelopes.ISerializable; + +namespace Sentry.Tests; + +[UsesVerify] +public class MetricTests +{ + public static IEnumerable GetMetrics() + { + var tags = new Dictionary + { + { "tag1", "value1" }, + { "tag2", "value2" } + }; + var counter = new CounterMetric("my.counter", 5, MeasurementUnit.Custom("counters"), tags); + yield return new object[] { counter }; + + var set = new SetMetric("my.set", 5, MeasurementUnit.Custom("sets"), tags); + set.Add(7); + yield return new object[]{ set }; + + var distribution = new DistributionMetric("my.distribution", 5, MeasurementUnit.Custom("distributions"), tags); + distribution.Add(7); + distribution.Add(13); + yield return new object[]{ distribution }; + + var gauge = new GaugeMetric("my.gauge", 5, MeasurementUnit.Custom("gauges"), tags); + gauge.Add(7); + yield return new object[]{ gauge }; + } + + [Theory] + [MemberData(nameof(GetMetrics))] + public async Task SerializeAsync_WritesMetric(ISentrySerializable metric) + { + // Arrange + var stream = new MemoryStream(); + + // Act + await metric.SerializeAsync(stream, null); + stream.Position = 0; + using var reader = new StreamReader(stream, Encoding.UTF8); + var statsd = await reader.ReadToEndAsync(); + + // Assert + await Verify(statsd); + } +} From 90be220b14d85b0ae8cf536e0dcffa0ccc18b089 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 11 Dec 2023 18:21:21 +1300 Subject: [PATCH 16/52] Updated verify tests --- ...ync_WritesMetric_metric=CounterMetric.verified.txt | 1 + ...ritesMetric_metric=DistributionMetric.verified.txt | 1 + ...Async_WritesMetric_metric=GaugeMetric.verified.txt | 1 + ...zeAsync_WritesMetric_metric=SetMetric.verified.txt | 1 + ...ts.Serialize_Counter_statsd.DotNet8_0.verified.txt | 1 + test/Sentry.Tests/MetricTests.verify.cs | 11 ++++++----- 6 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 test/Sentry.Tests/MetricTests.SerializeAsync_WritesMetric_metric=CounterMetric.verified.txt create mode 100644 test/Sentry.Tests/MetricTests.SerializeAsync_WritesMetric_metric=DistributionMetric.verified.txt create mode 100644 test/Sentry.Tests/MetricTests.SerializeAsync_WritesMetric_metric=GaugeMetric.verified.txt create mode 100644 test/Sentry.Tests/MetricTests.SerializeAsync_WritesMetric_metric=SetMetric.verified.txt create mode 100644 test/Sentry.Tests/MetricTests.Serialize_Counter_statsd.DotNet8_0.verified.txt diff --git a/test/Sentry.Tests/MetricTests.SerializeAsync_WritesMetric_metric=CounterMetric.verified.txt b/test/Sentry.Tests/MetricTests.SerializeAsync_WritesMetric_metric=CounterMetric.verified.txt new file mode 100644 index 0000000000..07089ed3ee --- /dev/null +++ b/test/Sentry.Tests/MetricTests.SerializeAsync_WritesMetric_metric=CounterMetric.verified.txt @@ -0,0 +1 @@ +my.counter@counters:5|c|#tag1:SanitizeValue(value),tag2:SanitizeValue(value)|T1577836800 diff --git a/test/Sentry.Tests/MetricTests.SerializeAsync_WritesMetric_metric=DistributionMetric.verified.txt b/test/Sentry.Tests/MetricTests.SerializeAsync_WritesMetric_metric=DistributionMetric.verified.txt new file mode 100644 index 0000000000..f6a5cd69e2 --- /dev/null +++ b/test/Sentry.Tests/MetricTests.SerializeAsync_WritesMetric_metric=DistributionMetric.verified.txt @@ -0,0 +1 @@ +my.distribution@distributions:5:7:13|d|#tag1:SanitizeValue(value),tag2:SanitizeValue(value)|T1577836800 diff --git a/test/Sentry.Tests/MetricTests.SerializeAsync_WritesMetric_metric=GaugeMetric.verified.txt b/test/Sentry.Tests/MetricTests.SerializeAsync_WritesMetric_metric=GaugeMetric.verified.txt new file mode 100644 index 0000000000..ed798ecca2 --- /dev/null +++ b/test/Sentry.Tests/MetricTests.SerializeAsync_WritesMetric_metric=GaugeMetric.verified.txt @@ -0,0 +1 @@ +my.gauge@gauges:7:5:7:12:2|g|#tag1:SanitizeValue(value),tag2:SanitizeValue(value)|T1577836800 diff --git a/test/Sentry.Tests/MetricTests.SerializeAsync_WritesMetric_metric=SetMetric.verified.txt b/test/Sentry.Tests/MetricTests.SerializeAsync_WritesMetric_metric=SetMetric.verified.txt new file mode 100644 index 0000000000..d147d1fc26 --- /dev/null +++ b/test/Sentry.Tests/MetricTests.SerializeAsync_WritesMetric_metric=SetMetric.verified.txt @@ -0,0 +1 @@ +my.set@sets:5:7|s|#tag1:SanitizeValue(value),tag2:SanitizeValue(value)|T1577836800 diff --git a/test/Sentry.Tests/MetricTests.Serialize_Counter_statsd.DotNet8_0.verified.txt b/test/Sentry.Tests/MetricTests.Serialize_Counter_statsd.DotNet8_0.verified.txt new file mode 100644 index 0000000000..1f67076aaf --- /dev/null +++ b/test/Sentry.Tests/MetricTests.Serialize_Counter_statsd.DotNet8_0.verified.txt @@ -0,0 +1 @@ +my.counter@widgets:5|c|#tag1:SanitizeValue(value),tag2:SanitizeValue(value)|T1702270140 diff --git a/test/Sentry.Tests/MetricTests.verify.cs b/test/Sentry.Tests/MetricTests.verify.cs index 269ba37a54..7c275137c0 100644 --- a/test/Sentry.Tests/MetricTests.verify.cs +++ b/test/Sentry.Tests/MetricTests.verify.cs @@ -8,24 +8,25 @@ public class MetricTests { public static IEnumerable GetMetrics() { + var timestamp = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc); var tags = new Dictionary { { "tag1", "value1" }, { "tag2", "value2" } }; - var counter = new CounterMetric("my.counter", 5, MeasurementUnit.Custom("counters"), tags); + var counter = new CounterMetric("my.counter", 5, MeasurementUnit.Custom("counters"), tags, timestamp); yield return new object[] { counter }; - var set = new SetMetric("my.set", 5, MeasurementUnit.Custom("sets"), tags); + var set = new SetMetric("my.set", 5, MeasurementUnit.Custom("sets"), tags, timestamp); set.Add(7); yield return new object[]{ set }; - var distribution = new DistributionMetric("my.distribution", 5, MeasurementUnit.Custom("distributions"), tags); + var distribution = new DistributionMetric("my.distribution", 5, MeasurementUnit.Custom("distributions"), tags, timestamp); distribution.Add(7); distribution.Add(13); yield return new object[]{ distribution }; - var gauge = new GaugeMetric("my.gauge", 5, MeasurementUnit.Custom("gauges"), tags); + var gauge = new GaugeMetric("my.gauge", 5, MeasurementUnit.Custom("gauges"), tags, timestamp); gauge.Add(7); yield return new object[]{ gauge }; } @@ -44,6 +45,6 @@ public async Task SerializeAsync_WritesMetric(ISentrySerializable metric) var statsd = await reader.ReadToEndAsync(); // Assert - await Verify(statsd); + await Verify(statsd).UseParameters(metric.GetType().Name); } } From 94790d70189ec3f41e4ab9f00ab7b1c398fba728 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 11 Dec 2023 20:17:08 +1300 Subject: [PATCH 17/52] Added Timing --- src/Sentry/DelegatingMetricAggregator.cs | 3 + src/Sentry/DisabledMetricAggregator.cs | 6 ++ src/Sentry/IMetricAggregator.cs | 19 ++++ src/Sentry/MeasurementUnit.cs | 2 +- src/Sentry/MetricAggregator.cs | 23 ++++- src/Sentry/Timing.cs | 94 +++++++++++++++++++ ...piApprovalTests.Run.DotNet6_0.verified.txt | 7 ++ ...piApprovalTests.Run.DotNet7_0.verified.txt | 7 ++ ...piApprovalTests.Run.DotNet8_0.verified.txt | 7 ++ .../ApiApprovalTests.Run.Net4_8.verified.txt | 7 ++ 10 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 src/Sentry/Timing.cs diff --git a/src/Sentry/DelegatingMetricAggregator.cs b/src/Sentry/DelegatingMetricAggregator.cs index 0f3e0b0f82..b5cc65dd12 100644 --- a/src/Sentry/DelegatingMetricAggregator.cs +++ b/src/Sentry/DelegatingMetricAggregator.cs @@ -13,4 +13,7 @@ public void Distribution(string key, double value = 1, MeasurementUnit? unit = n public void Set(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, DateTime? timestamp = null) => innerAggregator.Set(key, value, unit, tags, timestamp); + + public void Timing(string key, double value, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, IDictionary? tags = null, + DateTime? timestamp = null) => innerAggregator.Timing(key, value, unit, tags, timestamp); } diff --git a/src/Sentry/DisabledMetricAggregator.cs b/src/Sentry/DisabledMetricAggregator.cs index e087d700cf..0c7eff7af3 100644 --- a/src/Sentry/DisabledMetricAggregator.cs +++ b/src/Sentry/DisabledMetricAggregator.cs @@ -25,4 +25,10 @@ public void Set(string key, double value = 1, MeasurementUnit? unit = null, IDic { // No Op } + + public void Timing(string key, double value, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, IDictionary? tags = null, + DateTime? timestamp = null) + { + // No Op + } } diff --git a/src/Sentry/IMetricAggregator.cs b/src/Sentry/IMetricAggregator.cs index 790d3ee5a5..796ced57b3 100644 --- a/src/Sentry/IMetricAggregator.cs +++ b/src/Sentry/IMetricAggregator.cs @@ -81,4 +81,23 @@ void Set( DateTime? timestamp = null // , int stacklevel = 0 // Used for code locations ); + + /// + /// Emits a distribution with the time it takes to run a given code block. + /// + /// A unique key identifying the metric + /// The value to be added + /// + /// An optional . Defaults to + /// + /// Optional Tags to associate with the metric + /// The time when the metric was emitted + void Timing( + string key, + double value, + MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, + IDictionary? tags = null, + DateTime? timestamp = null + // , int stacklevel = 0 // Used for code locations + ); } diff --git a/src/Sentry/MeasurementUnit.cs b/src/Sentry/MeasurementUnit.cs index 2fe9776f09..b572e335bd 100644 --- a/src/Sentry/MeasurementUnit.cs +++ b/src/Sentry/MeasurementUnit.cs @@ -93,4 +93,4 @@ internal static MeasurementUnit Parse(string? name) /// Returns true if the operands are not equal. /// public static bool operator !=(MeasurementUnit left, MeasurementUnit right) => !left.Equals(right); -} \ No newline at end of file +} diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index a9826ccedf..cace93ad3b 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -24,10 +24,19 @@ private readonly Lazy>? captureMetrics = null, CancellationTokenSource? shutdownSource = null, bool disableLoopTask = false) + /// + /// MetricAggregator constructor. + /// + /// The + /// The callback to be called to transmit aggregated metrics to a statsd server + /// A + /// + /// A boolean value indicating whether the Loop to flush metrics should run. This is provided for unit testing only. + /// + public MetricAggregator(SentryOptions options, Action> captureMetrics, CancellationTokenSource? shutdownSource = null, bool disableLoopTask = false) { _options = options; - _captureMetrics = captureMetrics ?? (_ => { }); + _captureMetrics = captureMetrics; _shutdownSource = shutdownSource ?? new CancellationTokenSource(); if (disableLoopTask) @@ -98,6 +107,16 @@ public void Set( // , int stacklevel = 0 // Used for code locations ) => Emit(MetricType.Set, key, value, unit, tags, timestamp); + /// + public void Timing( + string key, + double value, + MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, + IDictionary? tags = null, + DateTime? timestamp = null) + // , int stacklevel = 0 // Used for code locations + => Emit(MetricType.Distribution, key, value, unit, tags, timestamp); + private void Emit( MetricType type, string key, diff --git a/src/Sentry/Timing.cs b/src/Sentry/Timing.cs new file mode 100644 index 0000000000..7815bd4dff --- /dev/null +++ b/src/Sentry/Timing.cs @@ -0,0 +1,94 @@ +using Sentry.Extensibility; + +namespace Sentry; + +/// +/// Measures the time it takes to run a given code block and emits this as a metric. The class is +/// designed to be used in a using statement. +/// +/// +/// using (var timing = new Timing("my-operation")) +/// { +/// ... +/// } +/// +public class Timing: IDisposable +{ + private readonly IHub _hub; + private readonly string _key; + private readonly MeasurementUnit.Duration _unit; + private readonly IDictionary? _tags; + private readonly Stopwatch _stopwatch = new(); + private readonly ISpan _span; + + /// + /// Creates a new instance. + /// + public Timing(string key, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, + IDictionary? tags = null) + : this(SentrySdk.CurrentHub, key, unit, tags) + { + } + + /// + /// Creates a new instance. + /// + public Timing(IHub hub, string key, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, + IDictionary? tags = null) + { + _hub = hub; + _key = key; + _unit = unit; + _tags = tags; + _stopwatch.Start(); + + ITransactionTracer? currentTransaction = null; + hub.ConfigureScope(s => currentTransaction = s.Transaction); + _span = currentTransaction is {} transaction + ? transaction.StartChild("metric.timing", key) + : hub.StartTransaction("metric.timing", key); + if (tags is not null) + { + foreach (var (k, v) in tags) + { + _span.SetTag(k, v); + } + } + + // # report code locations here for better accuracy + // aggregator = _get_aggregator() + // if aggregator is not None: + // aggregator.record_code_location("d", self.key, self.unit, self.stacklevel) + } + + /// + public void Dispose() + { + _stopwatch.Stop(); + + try + { + var value = _unit switch + { + MeasurementUnit.Duration.Week => _stopwatch.Elapsed.TotalDays / 7, + MeasurementUnit.Duration.Day => _stopwatch.Elapsed.TotalDays, + MeasurementUnit.Duration.Hour => _stopwatch.Elapsed.TotalHours, + MeasurementUnit.Duration.Minute => _stopwatch.Elapsed.TotalMinutes, + MeasurementUnit.Duration.Second => _stopwatch.Elapsed.TotalSeconds, + MeasurementUnit.Duration.Millisecond => _stopwatch.Elapsed.TotalMilliseconds, + MeasurementUnit.Duration.Microsecond => _stopwatch.Elapsed.TotalMilliseconds * 1000, + MeasurementUnit.Duration.Nanosecond => _stopwatch.Elapsed.TotalMilliseconds * 1000000, + _ => throw new ArgumentOutOfRangeException(nameof(_unit), _unit, null) + }; + _hub.Metrics.Timing(_key, value, _unit, _tags); + } + catch(Exception e) + { + _hub.GetSentryOptions()?.LogError(e, "Error capturing timing"); + } + finally + { + _span?.Finish(); + } + } +} diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt index e13150c4b7..bf6ca2b347 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt @@ -248,6 +248,7 @@ namespace Sentry void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); + void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); } public interface IScopeObserver { @@ -1001,6 +1002,12 @@ namespace Sentry public override string ToString() { } public static Sentry.SubstringOrRegexPattern op_Implicit(string substringOrRegexPattern) { } } + public class Timing : System.IDisposable + { + public Timing(string key, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null) { } + public Timing(Sentry.IHub hub, string key, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null) { } + public void Dispose() { } + } public class Transaction : Sentry.IEventLike, Sentry.IHasExtra, Sentry.IHasTags, Sentry.IJsonSerializable, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext { public Transaction(Sentry.ITransactionTracer tracer) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt index e13150c4b7..bf6ca2b347 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt @@ -248,6 +248,7 @@ namespace Sentry void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); + void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); } public interface IScopeObserver { @@ -1001,6 +1002,12 @@ namespace Sentry public override string ToString() { } public static Sentry.SubstringOrRegexPattern op_Implicit(string substringOrRegexPattern) { } } + public class Timing : System.IDisposable + { + public Timing(string key, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null) { } + public Timing(Sentry.IHub hub, string key, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null) { } + public void Dispose() { } + } public class Transaction : Sentry.IEventLike, Sentry.IHasExtra, Sentry.IHasTags, Sentry.IJsonSerializable, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext { public Transaction(Sentry.ITransactionTracer tracer) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 531b2da2d9..3478076751 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -249,6 +249,7 @@ namespace Sentry void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); + void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); } public interface IScopeObserver { @@ -1002,6 +1003,12 @@ namespace Sentry public override string ToString() { } public static Sentry.SubstringOrRegexPattern op_Implicit(string substringOrRegexPattern) { } } + public class Timing : System.IDisposable + { + public Timing(string key, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null) { } + public Timing(Sentry.IHub hub, string key, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null) { } + public void Dispose() { } + } public class Transaction : Sentry.IEventLike, Sentry.IHasExtra, Sentry.IHasTags, Sentry.IJsonSerializable, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext { public Transaction(Sentry.ITransactionTracer tracer) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 1819d1cd74..a06f0caed7 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -247,6 +247,7 @@ namespace Sentry void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); + void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); } public interface IScopeObserver { @@ -998,6 +999,12 @@ namespace Sentry public override string ToString() { } public static Sentry.SubstringOrRegexPattern op_Implicit(string substringOrRegexPattern) { } } + public class Timing : System.IDisposable + { + public Timing(string key, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null) { } + public Timing(Sentry.IHub hub, string key, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null) { } + public void Dispose() { } + } public class Transaction : Sentry.IEventLike, Sentry.IHasExtra, Sentry.IHasTags, Sentry.IJsonSerializable, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext { public Transaction(Sentry.ITransactionTracer tracer) { } From 54af5c28aeeb05d341a2ae3b4c902d837f79b7d3 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 11 Dec 2023 21:56:32 +1300 Subject: [PATCH 18/52] Added a (commented out) test to check if the aggregator is threadsafe --- src/Sentry/MetricAggregator.cs | 9 ++++--- src/Sentry/MetricBucketHelper.cs | 5 ++-- test/Sentry.Tests/MetricAggregatorTests.cs | 30 +++++++++++++++++++++- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index cace93ad3b..e056424929 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -10,7 +10,7 @@ internal enum MetricType : byte { Counter, Gauge, Distribution, Set } private readonly SentryOptions _options; private readonly Action> _captureMetrics; - private readonly TimeSpan _flushInterval = TimeSpan.FromSeconds(5); + private readonly TimeSpan _flushInterval; private readonly CancellationTokenSource _shutdownSource; private volatile bool _disposed; @@ -31,13 +31,16 @@ private readonly LazyThe callback to be called to transmit aggregated metrics to a statsd server /// A /// - /// A boolean value indicating whether the Loop to flush metrics should run. This is provided for unit testing only. + /// A boolean value indicating whether the Loop to flush metrics should run, for testing only. /// - public MetricAggregator(SentryOptions options, Action> captureMetrics, CancellationTokenSource? shutdownSource = null, bool disableLoopTask = false) + /// An optional flushInterval, for testing only + public MetricAggregator(SentryOptions options, Action> captureMetrics, + CancellationTokenSource? shutdownSource = null, bool disableLoopTask = false, TimeSpan? flushInterval = null) { _options = options; _captureMetrics = captureMetrics; _shutdownSource = shutdownSource ?? new CancellationTokenSource(); + _flushInterval = flushInterval ?? TimeSpan.FromSeconds(5); if (disableLoopTask) { diff --git a/src/Sentry/MetricBucketHelper.cs b/src/Sentry/MetricBucketHelper.cs index 03c4af374a..9f04c02c9f 100644 --- a/src/Sentry/MetricBucketHelper.cs +++ b/src/Sentry/MetricBucketHelper.cs @@ -18,8 +18,9 @@ internal static long GetTimeBucketKey(this DateTime timestamp) /// once per aggregator boot to achieve some level of offsetting across a fleet of deployed SDKs. Relay itself will /// also apply independent jittering. /// - private static readonly double _flushShift = new Random().NextDouble() * RollupInSeconds; + /// Internal for testing + internal static double FlushShift = new Random().NextDouble() * RollupInSeconds; internal static DateTime GetCutoff() => DateTime.UtcNow .Subtract(TimeSpan.FromSeconds(RollupInSeconds)) - .Subtract(TimeSpan.FromSeconds(_flushShift)); + .Subtract(TimeSpan.FromSeconds(FlushShift)); } diff --git a/test/Sentry.Tests/MetricAggregatorTests.cs b/test/Sentry.Tests/MetricAggregatorTests.cs index 1a9d509a7f..c397045248 100644 --- a/test/Sentry.Tests/MetricAggregatorTests.cs +++ b/test/Sentry.Tests/MetricAggregatorTests.cs @@ -9,8 +9,9 @@ class Fixture public SentryOptions Options { get; set; } = new(); public Action> CaptureMetrics { get; set; } = (_ => { }); public bool DisableFlushLoop { get; set; } = true; + public TimeSpan FlushInterval { get; set; } = TimeSpan.FromMilliseconds(100); public MetricAggregator GetSut() - => new(Options, CaptureMetrics, disableLoopTask: DisableFlushLoop); + => new(Options, CaptureMetrics, disableLoopTask: DisableFlushLoop, flushInterval: FlushInterval); } private readonly Fixture _fixture = new(); @@ -163,4 +164,31 @@ public void Set_AggregatesMetrics() var data2 = (SetMetric)bucket2[MetricAggregator.GetMetricBucketKey(metricType, key, unit, tags)]; data2.Value.Should().BeEquivalentTo(new[] {13}); } + + // [Fact] + // public async Task GetFlushableBuckets_IsThreadsafe() + // { + // // Arrange + // var rnd = new Random(); + // MetricBucketHelper.FlushShift = 0.0; + // _fixture.DisableFlushLoop = false; + // _fixture.CaptureMetrics = _ => Thread.Sleep(rnd.Next(10, 20)); + // var sut = _fixture.GetSut(); + // + // // Act... spawn some threads that add loads of metrics + // var started = DateTime.UtcNow; + // while (DateTime.UtcNow - started < TimeSpan.FromSeconds(600)) + // { + // for (var i = 0; i < 100; i++) + // { + // await Task.Run(() => sut.Gauge("meter", rnd.NextDouble())); + // } + // } + // + // // Wait for the flush loop to clear everything out + // await Task.Delay(TimeSpan.FromMilliseconds(500)); + // + // // Assert + // sut.Buckets.Should().BeEmpty(); // Ensures no metrics were added to a bucket after it was removed + // } } From 9344f385d5f20b67243019b296b66651be63b002 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 12 Dec 2023 20:28:29 +1300 Subject: [PATCH 19/52] Fixed concurrency issue (could still make this more performant) --- src/Sentry/MetricAggregator.cs | 166 ++++++++++++++++----- src/Sentry/MetricBucketHelper.cs | 12 +- src/Sentry/Timing.cs | 1 + test/Sentry.Tests/MetricAggregatorTests.cs | 73 +++++---- 4 files changed, 184 insertions(+), 68 deletions(-) diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index e056424929..9cb4067147 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -22,6 +22,10 @@ internal enum MetricType : byte { Counter, Gauge, Distribution, Set } private readonly Lazy>> _buckets = new(() => new ConcurrentDictionary>()); + // TODO: Initialize seen_locations + // self._seen_locations = _set() # type: Set[Tuple[int, MetricMetaKey]] + // self._pending_locations = {} # type: Dict[int, List[Tuple[MetricMetaKey, Any]]] + private Task LoopTask { get; } /// @@ -120,6 +124,7 @@ public void Timing( // , int stacklevel = 0 // Used for code locations => Emit(MetricType.Distribution, key, value, unit, tags, timestamp); + private object _bucketLock = new object(); private void Emit( MetricType type, string key, @@ -132,31 +137,70 @@ private void Emit( { timestamp ??= DateTime.UtcNow; unit ??= MeasurementUnit.None; - var timeBucket = Buckets.GetOrAdd( - timestamp.Value.GetTimeBucketKey(), - _ => new ConcurrentDictionary() - ); - Func addValuesFactory = type switch + lock (_bucketLock) { - MetricType.Counter => (string _) => new CounterMetric(key, value, unit.Value, tags, timestamp), - MetricType.Gauge => (string _) => new GaugeMetric(key, value, unit.Value, tags, timestamp), - MetricType.Distribution => (string _) => new DistributionMetric(key, value, unit.Value, tags, timestamp), - MetricType.Set => (string _) => new SetMetric(key, (int)value, unit.Value, tags, timestamp), - _ => throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown MetricType") - }; + var timeBucket = Buckets.GetOrAdd( + timestamp.Value.GetTimeBucketKey(), + _ => new ConcurrentDictionary() + ); - timeBucket.AddOrUpdate( - GetMetricBucketKey(type, key, unit.Value, tags), - addValuesFactory, - (_, metric) => + Func addValuesFactory = type switch { - metric.Add(value); - return metric; - } - ); + MetricType.Counter => _ => new CounterMetric(key, value, unit.Value, tags, timestamp), + MetricType.Gauge => _ => new GaugeMetric(key, value, unit.Value, tags, timestamp), + MetricType.Distribution => _ => new DistributionMetric(key, value, unit.Value, tags, timestamp), + MetricType.Set => _ => new SetMetric(key, (int)value, unit.Value, tags, timestamp), + _ => throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown MetricType") + }; + + timeBucket.AddOrUpdate( + GetMetricBucketKey(type, key, unit.Value, tags), + addValuesFactory, + (_, metric) => + { + metric.Add(value); + return metric; + } + ); + } + + // TODO: record the code location + // if stacklevel is not None: + // self.record_code_location(ty, key, unit, stacklevel + 2, timestamp) + } + // TODO: record_code_location + // def record_code_location( + // self, + // ty, # type: MetricType + // key, # type: str + // unit, # type: MeasurementUnit + // stacklevel, # type: int + // timestamp=None, # type: Optional[float] + // ): + // # type: (...) -> None + // if not self._enable_code_locations: + // return + // if timestamp is None: + // timestamp = time.time() + // meta_key = (ty, key, unit) + // start_of_day = utc_from_timestamp(timestamp).replace( + // hour=0, minute=0, second=0, microsecond=0, tzinfo=None + // ) + // start_of_day = int(to_timestamp(start_of_day)) + // + // if (start_of_day, meta_key) not in self._seen_locations: + // self._seen_locations.add((start_of_day, meta_key)) + // loc = get_code_location(stacklevel + 3) + // if loc is not None: + // # Group metadata by day to make flushing more efficient. + // # There needs to be one envelope item per timestamp. + // self._pending_locations.setdefault(start_of_day, []).append( + // (meta_key, loc) + // ) + private async Task RunLoopAsync() { _options.LogDebug("MetricsAggregator Started."); @@ -197,13 +241,37 @@ private async Task RunLoopAsync() } } - // Work with the envelope while it's in the queue - foreach (var key in GetFlushableBuckets()) + if (shutdownRequested || !Flush()) + { + return; + } + } + } + catch (Exception e) + { + _options.LogFatal(e, "Exception in the Metric Aggregator."); + throw; + } + } + + private readonly object _flushLock = new(); + + /// + /// Flushes any flushable buckets. + /// If is true then the cutoff is ignored and all buckets are flushed. + /// + /// Forces all buckets to be flushed, ignoring the cutoff + /// False if a shutdown is requested during flush, true otherwise + private bool Flush(bool force = false) + { + // We don't want multiple flushes happening concurrently... which might be possible if the regular flush loop + // triggered a flush at the same time ForceFlush is called + lock(_flushLock) + { + lock (_bucketLock) + { + foreach (var key in GetFlushableBuckets(force)) { - if (shutdownRequested) - { - break; - } try { _options.LogDebug("Flushing metrics for bucket {0}", key); @@ -216,7 +284,7 @@ private async Task RunLoopAsync() catch (OperationCanceledException) { _options.LogInfo("Shutdown token triggered. Time to exit."); - return; + return false; } catch (Exception exception) { @@ -224,38 +292,62 @@ private async Task RunLoopAsync() } } } + + // TODO: Flush the code locations + // for timestamp, locations in GetFlushableLocations()): + // encoded_locations = _encode_locations(timestamp, locations) + // envelope.add_item(Item(payload=encoded_locations, type="metric_meta")) } - catch (Exception e) - { - _options.LogFatal(e, "Exception in the Metric Aggregator."); - throw; - } + + return true; } + internal bool ForceFlush() => Flush(true); + /// /// Returns the keys for any buckets that are ready to be flushed (i.e. are for periods before the cutoff) /// + /// Forces all buckets to be flushed, ignoring the cutoff /// /// An enumerable containing the keys for any buckets that are ready to be flushed /// - internal IEnumerable GetFlushableBuckets() + internal IEnumerable GetFlushableBuckets(bool force = false) { if (!_buckets.IsValueCreated) { yield break; } - var cutoff = MetricBucketHelper.GetCutoff(); - foreach (var bucket in Buckets) + if (force) { - var bucketTime = DateTimeOffset.FromUnixTimeSeconds(bucket.Key); - if (bucketTime < cutoff) + // Return all the buckets in this case + foreach (var key in Buckets.Keys) { - yield return bucket.Key; + yield return key; + } + } + else + { + var cutoff = MetricBucketHelper.GetCutoff(); + foreach (var key in Buckets.Keys) + { + var bucketTime = DateTimeOffset.FromUnixTimeSeconds(key); + if (bucketTime < cutoff) + { + yield return key; + } } } } + // TODO: _flushable_locations + // def _flushable_locations(self): + // # type: (...) -> Dict[int, List[Tuple[MetricMetaKey, Dict[str, Any]]]] + // with self._lock: + // locations = self._pending_locations + // self._pending_locations = {} + // return locations + /// /// Stops the background worker and waits for it to empty the queue until 'shutdownTimeout' is reached /// diff --git a/src/Sentry/MetricBucketHelper.cs b/src/Sentry/MetricBucketHelper.cs index 9f04c02c9f..1e9db01175 100644 --- a/src/Sentry/MetricBucketHelper.cs +++ b/src/Sentry/MetricBucketHelper.cs @@ -4,11 +4,15 @@ internal static class MetricBucketHelper { private const int RollupInSeconds = 10; - private static readonly DateTime EpochStart = new(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); +#if NET6_0_OR_GREATER + static readonly DateTime UnixEpoch = DateTime.UnixEpoch; +#else + static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); +#endif internal static long GetTimeBucketKey(this DateTime timestamp) { - var seconds = (long)(timestamp.ToUniversalTime() - EpochStart).TotalSeconds; + var seconds = (long)(timestamp.ToUniversalTime() - UnixEpoch).TotalSeconds; return (seconds / RollupInSeconds) * RollupInSeconds; } @@ -19,8 +23,8 @@ internal static long GetTimeBucketKey(this DateTime timestamp) /// also apply independent jittering. /// /// Internal for testing - internal static double FlushShift = new Random().NextDouble() * RollupInSeconds; + internal static double FlushShift = new Random().Next(0, 1000) * RollupInSeconds; internal static DateTime GetCutoff() => DateTime.UtcNow .Subtract(TimeSpan.FromSeconds(RollupInSeconds)) - .Subtract(TimeSpan.FromSeconds(FlushShift)); + .Subtract(TimeSpan.FromMilliseconds(FlushShift)); } diff --git a/src/Sentry/Timing.cs b/src/Sentry/Timing.cs index 7815bd4dff..d023452c50 100644 --- a/src/Sentry/Timing.cs +++ b/src/Sentry/Timing.cs @@ -55,6 +55,7 @@ public Timing(IHub hub, string key, MeasurementUnit.Duration unit = MeasurementU } } + // TODO: Record the code location // # report code locations here for better accuracy // aggregator = _get_aggregator() // if aggregator is not None: diff --git a/test/Sentry.Tests/MetricAggregatorTests.cs b/test/Sentry.Tests/MetricAggregatorTests.cs index c397045248..6938087c7f 100644 --- a/test/Sentry.Tests/MetricAggregatorTests.cs +++ b/test/Sentry.Tests/MetricAggregatorTests.cs @@ -14,7 +14,8 @@ public MetricAggregator GetSut() => new(Options, CaptureMetrics, disableLoopTask: DisableFlushLoop, flushInterval: FlushInterval); } - private readonly Fixture _fixture = new(); + // private readonly Fixture _fixture = new(); + private static readonly Fixture _fixture = new(); [Fact] public void GetMetricBucketKey_GeneratesExpectedKey() @@ -165,30 +166,48 @@ public void Set_AggregatesMetrics() data2.Value.Should().BeEquivalentTo(new[] {13}); } - // [Fact] - // public async Task GetFlushableBuckets_IsThreadsafe() - // { - // // Arrange - // var rnd = new Random(); - // MetricBucketHelper.FlushShift = 0.0; - // _fixture.DisableFlushLoop = false; - // _fixture.CaptureMetrics = _ => Thread.Sleep(rnd.Next(10, 20)); - // var sut = _fixture.GetSut(); - // - // // Act... spawn some threads that add loads of metrics - // var started = DateTime.UtcNow; - // while (DateTime.UtcNow - started < TimeSpan.FromSeconds(600)) - // { - // for (var i = 0; i < 100; i++) - // { - // await Task.Run(() => sut.Gauge("meter", rnd.NextDouble())); - // } - // } - // - // // Wait for the flush loop to clear everything out - // await Task.Delay(TimeSpan.FromMilliseconds(500)); - // - // // Assert - // sut.Buckets.Should().BeEmpty(); // Ensures no metrics were added to a bucket after it was removed - // } + [Fact] + public void GetFlushableBuckets_IsThreadsafe() + { + // Arrange + const int numThreads = 100; + const int numThreadIterations = 1000; + var sent = 0; + MetricBucketHelper.FlushShift = 0.0; + _fixture.DisableFlushLoop = false; + _fixture.CaptureMetrics = metrics => + { + foreach (var metric in metrics) + { + Interlocked.Add(ref sent, (int)((CounterMetric)metric).Value); + } + }; + var sut = _fixture.GetSut(); + + // Act... spawn some threads that add loads of metrics + var resetEvent = new ManualResetEvent(false); + var toProcess = numThreads; + for(var i = 0; i < numThreads; i++) + { + new Thread(delegate() + { + for(var i = 0; i < numThreadIterations; i++) + { + sut.Increment("counter"); + } + // If we're the last thread, signal + if (Interlocked.Decrement(ref toProcess) == 0) + { + resetEvent.Set(); + } + }).Start(); + } + + // Wait for workers. + resetEvent.WaitOne(); + sut.ForceFlush(); + + // Assert + sent.Should().Be(numThreads * numThreadIterations); + } } From 58276089947ab569288a3d2fd929eae4fea9cfd6 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 12 Dec 2023 21:28:22 +1300 Subject: [PATCH 20/52] Reduced the scope of the lock for updating metrics --- src/Sentry/MetricAggregator.cs | 78 +++++++++++++++------------------- 1 file changed, 35 insertions(+), 43 deletions(-) diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index 9cb4067147..469962f170 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -124,7 +124,7 @@ public void Timing( // , int stacklevel = 0 // Used for code locations => Emit(MetricType.Distribution, key, value, unit, tags, timestamp); - private object _bucketLock = new object(); + private readonly object _emitLock = new(); private void Emit( MetricType type, string key, @@ -138,37 +138,32 @@ private void Emit( timestamp ??= DateTime.UtcNow; unit ??= MeasurementUnit.None; - lock (_bucketLock) - { - var timeBucket = Buckets.GetOrAdd( - timestamp.Value.GetTimeBucketKey(), - _ => new ConcurrentDictionary() - ); - - Func addValuesFactory = type switch - { - MetricType.Counter => _ => new CounterMetric(key, value, unit.Value, tags, timestamp), - MetricType.Gauge => _ => new GaugeMetric(key, value, unit.Value, tags, timestamp), - MetricType.Distribution => _ => new DistributionMetric(key, value, unit.Value, tags, timestamp), - MetricType.Set => _ => new SetMetric(key, (int)value, unit.Value, tags, timestamp), - _ => throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown MetricType") - }; + var timeBucket = Buckets.GetOrAdd( + timestamp.Value.GetTimeBucketKey(), + _ => new ConcurrentDictionary() + ); - timeBucket.AddOrUpdate( - GetMetricBucketKey(type, key, unit.Value, tags), - addValuesFactory, - (_, metric) => - { - metric.Add(value); - return metric; - } - ); + var metric = timeBucket.GetOrAdd( + GetMetricBucketKey(type, key, unit.Value, tags), + _ => AddValues(timestamp.Value)); + lock (_emitLock) + { + metric.Add(value); } // TODO: record the code location // if stacklevel is not None: // self.record_code_location(ty, key, unit, stacklevel + 2, timestamp) + Metric AddValues(DateTime ts) => + type switch + { + MetricType.Counter => new CounterMetric(key, value, unit.Value, tags, ts), + MetricType.Gauge => new GaugeMetric(key, value, unit.Value, tags, ts), + MetricType.Distribution => new DistributionMetric(key, value, unit.Value, tags, ts), + MetricType.Set => new SetMetric(key, (int)value, unit.Value, tags, ts), + _ => throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown MetricType") + }; } // TODO: record_code_location @@ -268,29 +263,26 @@ private bool Flush(bool force = false) // triggered a flush at the same time ForceFlush is called lock(_flushLock) { - lock (_bucketLock) + foreach (var key in GetFlushableBuckets(force)) { - foreach (var key in GetFlushableBuckets(force)) + try { - try - { - _options.LogDebug("Flushing metrics for bucket {0}", key); - if (Buckets.TryRemove(key, out var bucket)) - { - _captureMetrics(bucket.Values); - _options.LogDebug("Metric flushed for bucket {0}", key); - } - } - catch (OperationCanceledException) - { - _options.LogInfo("Shutdown token triggered. Time to exit."); - return false; - } - catch (Exception exception) + _options.LogDebug("Flushing metrics for bucket {0}", key); + if (Buckets.TryRemove(key, out var bucket)) { - _options.LogError(exception, "Error while processing metric aggregates."); + _captureMetrics(bucket.Values); + _options.LogDebug("Metric flushed for bucket {0}", key); } } + catch (OperationCanceledException) + { + _options.LogInfo("Shutdown token triggered. Time to exit."); + return false; + } + catch (Exception exception) + { + _options.LogError(exception, "Error while processing metric aggregates."); + } } // TODO: Flush the code locations From f3275acf120d1adb775cc96a29a083751cd7f2e9 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 13 Dec 2023 09:44:25 +1300 Subject: [PATCH 21/52] Fixed unit tests --- src/Sentry/MetricAggregator.cs | 78 ++++++++++++---------- test/Sentry.Tests/MetricAggregatorTests.cs | 3 +- 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index 469962f170..9cb4067147 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -124,7 +124,7 @@ public void Timing( // , int stacklevel = 0 // Used for code locations => Emit(MetricType.Distribution, key, value, unit, tags, timestamp); - private readonly object _emitLock = new(); + private object _bucketLock = new object(); private void Emit( MetricType type, string key, @@ -138,32 +138,37 @@ private void Emit( timestamp ??= DateTime.UtcNow; unit ??= MeasurementUnit.None; - var timeBucket = Buckets.GetOrAdd( - timestamp.Value.GetTimeBucketKey(), - _ => new ConcurrentDictionary() - ); - - var metric = timeBucket.GetOrAdd( - GetMetricBucketKey(type, key, unit.Value, tags), - _ => AddValues(timestamp.Value)); - lock (_emitLock) + lock (_bucketLock) { - metric.Add(value); + var timeBucket = Buckets.GetOrAdd( + timestamp.Value.GetTimeBucketKey(), + _ => new ConcurrentDictionary() + ); + + Func addValuesFactory = type switch + { + MetricType.Counter => _ => new CounterMetric(key, value, unit.Value, tags, timestamp), + MetricType.Gauge => _ => new GaugeMetric(key, value, unit.Value, tags, timestamp), + MetricType.Distribution => _ => new DistributionMetric(key, value, unit.Value, tags, timestamp), + MetricType.Set => _ => new SetMetric(key, (int)value, unit.Value, tags, timestamp), + _ => throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown MetricType") + }; + + timeBucket.AddOrUpdate( + GetMetricBucketKey(type, key, unit.Value, tags), + addValuesFactory, + (_, metric) => + { + metric.Add(value); + return metric; + } + ); } // TODO: record the code location // if stacklevel is not None: // self.record_code_location(ty, key, unit, stacklevel + 2, timestamp) - Metric AddValues(DateTime ts) => - type switch - { - MetricType.Counter => new CounterMetric(key, value, unit.Value, tags, ts), - MetricType.Gauge => new GaugeMetric(key, value, unit.Value, tags, ts), - MetricType.Distribution => new DistributionMetric(key, value, unit.Value, tags, ts), - MetricType.Set => new SetMetric(key, (int)value, unit.Value, tags, ts), - _ => throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown MetricType") - }; } // TODO: record_code_location @@ -263,25 +268,28 @@ private bool Flush(bool force = false) // triggered a flush at the same time ForceFlush is called lock(_flushLock) { - foreach (var key in GetFlushableBuckets(force)) + lock (_bucketLock) { - try + foreach (var key in GetFlushableBuckets(force)) { - _options.LogDebug("Flushing metrics for bucket {0}", key); - if (Buckets.TryRemove(key, out var bucket)) + try { - _captureMetrics(bucket.Values); - _options.LogDebug("Metric flushed for bucket {0}", key); + _options.LogDebug("Flushing metrics for bucket {0}", key); + if (Buckets.TryRemove(key, out var bucket)) + { + _captureMetrics(bucket.Values); + _options.LogDebug("Metric flushed for bucket {0}", key); + } + } + catch (OperationCanceledException) + { + _options.LogInfo("Shutdown token triggered. Time to exit."); + return false; + } + catch (Exception exception) + { + _options.LogError(exception, "Error while processing metric aggregates."); } - } - catch (OperationCanceledException) - { - _options.LogInfo("Shutdown token triggered. Time to exit."); - return false; - } - catch (Exception exception) - { - _options.LogError(exception, "Error while processing metric aggregates."); } } diff --git a/test/Sentry.Tests/MetricAggregatorTests.cs b/test/Sentry.Tests/MetricAggregatorTests.cs index 6938087c7f..eace7518bb 100644 --- a/test/Sentry.Tests/MetricAggregatorTests.cs +++ b/test/Sentry.Tests/MetricAggregatorTests.cs @@ -9,7 +9,7 @@ class Fixture public SentryOptions Options { get; set; } = new(); public Action> CaptureMetrics { get; set; } = (_ => { }); public bool DisableFlushLoop { get; set; } = true; - public TimeSpan FlushInterval { get; set; } = TimeSpan.FromMilliseconds(100); + public TimeSpan? FlushInterval { get; set; } public MetricAggregator GetSut() => new(Options, CaptureMetrics, disableLoopTask: DisableFlushLoop, flushInterval: FlushInterval); } @@ -175,6 +175,7 @@ public void GetFlushableBuckets_IsThreadsafe() var sent = 0; MetricBucketHelper.FlushShift = 0.0; _fixture.DisableFlushLoop = false; + _fixture.FlushInterval = TimeSpan.FromMilliseconds(100); _fixture.CaptureMetrics = metrics => { foreach (var metric in metrics) From 0f8615870c97063fc3fac6ac4051abc3e13a9c17 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 13 Dec 2023 10:03:35 +1300 Subject: [PATCH 22/52] Update MetricAggregator.cs --- src/Sentry/MetricAggregator.cs | 67 ++++++++++++++++------------------ 1 file changed, 31 insertions(+), 36 deletions(-) diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index 9cb4067147..92a16216e1 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -124,7 +124,7 @@ public void Timing( // , int stacklevel = 0 // Used for code locations => Emit(MetricType.Distribution, key, value, unit, tags, timestamp); - private object _bucketLock = new object(); + private readonly object _emitLock = new object(); private void Emit( MetricType type, string key, @@ -138,31 +138,29 @@ private void Emit( timestamp ??= DateTime.UtcNow; unit ??= MeasurementUnit.None; - lock (_bucketLock) + Func addValuesFactory = type switch { - var timeBucket = Buckets.GetOrAdd( - timestamp.Value.GetTimeBucketKey(), - _ => new ConcurrentDictionary() - ); + MetricType.Counter => _ => new CounterMetric(key, value, unit.Value, tags, timestamp), + MetricType.Gauge => _ => new GaugeMetric(key, value, unit.Value, tags, timestamp), + MetricType.Distribution => _ => new DistributionMetric(key, value, unit.Value, tags, timestamp), + MetricType.Set => _ => new SetMetric(key, (int)value, unit.Value, tags, timestamp), + _ => throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown MetricType") + }; - Func addValuesFactory = type switch - { - MetricType.Counter => _ => new CounterMetric(key, value, unit.Value, tags, timestamp), - MetricType.Gauge => _ => new GaugeMetric(key, value, unit.Value, tags, timestamp), - MetricType.Distribution => _ => new DistributionMetric(key, value, unit.Value, tags, timestamp), - MetricType.Set => _ => new SetMetric(key, (int)value, unit.Value, tags, timestamp), - _ => throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown MetricType") - }; + var timeBucket = Buckets.GetOrAdd( + timestamp.Value.GetTimeBucketKey(), + _ => new ConcurrentDictionary() + ); + lock (_emitLock) + { timeBucket.AddOrUpdate( GetMetricBucketKey(type, key, unit.Value, tags), addValuesFactory, - (_, metric) => - { + (_, metric) => { metric.Add(value); return metric; - } - ); + }); } // TODO: record the code location @@ -268,29 +266,26 @@ private bool Flush(bool force = false) // triggered a flush at the same time ForceFlush is called lock(_flushLock) { - lock (_bucketLock) + foreach (var key in GetFlushableBuckets(force)) { - foreach (var key in GetFlushableBuckets(force)) + try { - try + _options.LogDebug("Flushing metrics for bucket {0}", key); + if (Buckets.TryRemove(key, out var bucket)) { - _options.LogDebug("Flushing metrics for bucket {0}", key); - if (Buckets.TryRemove(key, out var bucket)) - { - _captureMetrics(bucket.Values); - _options.LogDebug("Metric flushed for bucket {0}", key); - } - } - catch (OperationCanceledException) - { - _options.LogInfo("Shutdown token triggered. Time to exit."); - return false; - } - catch (Exception exception) - { - _options.LogError(exception, "Error while processing metric aggregates."); + _captureMetrics(bucket.Values); + _options.LogDebug("Metric flushed for bucket {0}", key); } } + catch (OperationCanceledException) + { + _options.LogInfo("Shutdown token triggered. Time to exit."); + return false; + } + catch (Exception exception) + { + _options.LogError(exception, "Error while processing metric aggregates."); + } } // TODO: Flush the code locations From 7a75155bcadc189ad46a66a6033ce20b8413806c Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 14 Dec 2023 15:55:04 +1300 Subject: [PATCH 23/52] Initial implementation of Code Locations --- Sentry-CI-Build-macOS.slnf | 9 +- Sentry.sln | 7 + .../Sentry.Samples.Console.Metrics/Program.cs | 85 +++++++ .../Sentry.Samples.Console.Metrics.csproj | 12 + src/Sentry/DelegatingMetricAggregator.cs | 10 +- src/Sentry/DisabledMetricAggregator.cs | 10 +- src/Sentry/IMetricAggregator.cs | 25 +- src/Sentry/MetricAggregator.cs | 217 +++++++++++------- ...{MetricBucketHelper.cs => MetricHelper.cs} | 12 +- src/Sentry/Protocol/Envelopes/Envelope.cs | 13 ++ src/Sentry/Protocol/Envelopes/EnvelopeItem.cs | 15 ++ src/Sentry/Protocol/Metrics/CodeLocations.cs | 34 +++ src/Sentry/Protocol/Metrics/Metric.cs | 7 +- .../Metrics/MetricResourceIdentifier.cs | 6 + src/Sentry/Protocol/Metrics/MetricType.cs | 3 + src/Sentry/SentryClient.cs | 20 +- src/Sentry/SentryOptions.cs | 2 +- src/Sentry/SentryStackFrame.cs | 13 ++ ...piApprovalTests.Run.DotNet6_0.verified.txt | 12 +- ...piApprovalTests.Run.DotNet7_0.verified.txt | 12 +- ...piApprovalTests.Run.DotNet8_0.verified.txt | 12 +- .../ApiApprovalTests.Run.Net4_8.verified.txt | 12 +- test/Sentry.Tests/MetricAggregatorTests.cs | 31 ++- test/Sentry.Tests/MetricBucketHelperTests.cs | 16 ++ 24 files changed, 441 insertions(+), 154 deletions(-) create mode 100644 samples/Sentry.Samples.Console.Metrics/Program.cs create mode 100644 samples/Sentry.Samples.Console.Metrics/Sentry.Samples.Console.Metrics.csproj rename src/Sentry/{MetricBucketHelper.cs => MetricHelper.cs} (69%) create mode 100644 src/Sentry/Protocol/Metrics/CodeLocations.cs create mode 100644 src/Sentry/Protocol/Metrics/MetricResourceIdentifier.cs create mode 100644 src/Sentry/Protocol/Metrics/MetricType.cs diff --git a/Sentry-CI-Build-macOS.slnf b/Sentry-CI-Build-macOS.slnf index 336e58f89e..248ad15d66 100644 --- a/Sentry-CI-Build-macOS.slnf +++ b/Sentry-CI-Build-macOS.slnf @@ -15,6 +15,7 @@ "samples\\Sentry.Samples.Azure.Functions.Worker\\Sentry.Samples.Azure.Functions.Worker.csproj", "samples\\Sentry.Samples.Console.Basic\\Sentry.Samples.Console.Basic.csproj", "samples\\Sentry.Samples.Console.Customized\\Sentry.Samples.Console.Customized.csproj", + "samples\\Sentry.Samples.Console.Metrics\\Sentry.Samples.Console.Metrics.csproj", "samples\\Sentry.Samples.Console.Native\\Sentry.Samples.Console.Native.csproj", "samples\\Sentry.Samples.Console.Profiling\\Sentry.Samples.Console.Profiling.csproj", "samples\\Sentry.Samples.EntityFramework\\Sentry.Samples.EntityFramework.csproj", @@ -24,17 +25,17 @@ "samples\\Sentry.Samples.GraphQL.Server\\Sentry.Samples.GraphQL.Server.csproj", "samples\\Sentry.Samples.Ios\\Sentry.Samples.Ios.csproj", "samples\\Sentry.Samples.Log4Net\\Sentry.Samples.Log4Net.csproj", + "samples\\Sentry.Samples.ME.Logging\\Sentry.Samples.ME.Logging.csproj", "samples\\Sentry.Samples.MacCatalyst\\Sentry.Samples.MacCatalyst.csproj", "samples\\Sentry.Samples.Maui\\Sentry.Samples.Maui.csproj", - "samples\\Sentry.Samples.ME.Logging\\Sentry.Samples.ME.Logging.csproj", "samples\\Sentry.Samples.NLog\\Sentry.Samples.NLog.csproj", "samples\\Sentry.Samples.OpenTelemetry.AspNetCore\\Sentry.Samples.OpenTelemetry.AspNetCore.csproj", "samples\\Sentry.Samples.OpenTelemetry.Console\\Sentry.Samples.OpenTelemetry.Console.csproj", "samples\\Sentry.Samples.Serilog\\Sentry.Samples.Serilog.csproj", "src\\Sentry.Android.AssemblyReader\\Sentry.Android.AssemblyReader.csproj", - "src\\Sentry.AspNet\\Sentry.AspNet.csproj", "src\\Sentry.AspNetCore.Grpc\\Sentry.AspNetCore.Grpc.csproj", "src\\Sentry.AspNetCore\\Sentry.AspNetCore.csproj", + "src\\Sentry.AspNet\\Sentry.AspNet.csproj", "src\\Sentry.Azure.Functions.Worker\\Sentry.Azure.Functions.Worker.csproj", "src\\Sentry.Bindings.Android\\Sentry.Bindings.Android.csproj", "src\\Sentry.Bindings.Cocoa\\Sentry.Bindings.Cocoa.csproj", @@ -52,8 +53,8 @@ "test\\Sentry.Android.AssemblyReader.Tests\\Sentry.Android.AssemblyReader.Tests.csproj", "test\\Sentry.AspNet.Tests\\Sentry.AspNet.Tests.csproj", "test\\Sentry.AspNetCore.Grpc.Tests\\Sentry.AspNetCore.Grpc.Tests.csproj", - "test\\Sentry.AspNetCore.Tests\\Sentry.AspNetCore.Tests.csproj", "test\\Sentry.AspNetCore.TestUtils\\Sentry.AspNetCore.TestUtils.csproj", + "test\\Sentry.AspNetCore.Tests\\Sentry.AspNetCore.Tests.csproj", "test\\Sentry.Azure.Functions.Worker.Tests\\Sentry.Azure.Functions.Worker.Tests.csproj", "test\\Sentry.DiagnosticSource.IntegrationTests\\Sentry.DiagnosticSource.IntegrationTests.csproj", "test\\Sentry.DiagnosticSource.Tests\\Sentry.DiagnosticSource.Tests.csproj", @@ -72,4 +73,4 @@ "test\\SingleFileTestApp\\SingleFileTestApp.csproj" ] } -} +} \ No newline at end of file diff --git a/Sentry.sln b/Sentry.sln index ac60374e4b..3b32ad1694 100644 --- a/Sentry.sln +++ b/Sentry.sln @@ -163,6 +163,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FastSerialization", "module EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Samples.Console.Native", "samples\Sentry.Samples.Console.Native\Sentry.Samples.Console.Native.csproj", "{FC8AEABA-1A40-4891-9EBA-4B6A1F7244B2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Samples.Console.Metrics", "samples\Sentry.Samples.Console.Metrics\Sentry.Samples.Console.Metrics.csproj", "{BD2D08FC-8675-4157-A73C-D75F6A3856D3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -465,6 +467,10 @@ Global {FC8AEABA-1A40-4891-9EBA-4B6A1F7244B2}.Debug|Any CPU.Build.0 = Debug|Any CPU {FC8AEABA-1A40-4891-9EBA-4B6A1F7244B2}.Release|Any CPU.ActiveCfg = Release|Any CPU {FC8AEABA-1A40-4891-9EBA-4B6A1F7244B2}.Release|Any CPU.Build.0 = Release|Any CPU + {BD2D08FC-8675-4157-A73C-D75F6A3856D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BD2D08FC-8675-4157-A73C-D75F6A3856D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BD2D08FC-8675-4157-A73C-D75F6A3856D3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BD2D08FC-8675-4157-A73C-D75F6A3856D3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -545,5 +551,6 @@ Global {67269916-C417-4CEE-BD7D-CA66C3830AEE} = {A3CCA27E-4DF8-479D-833C-CAA0950715AA} {8032310D-3C06-442C-A318-F365BCC4C804} = {A3CCA27E-4DF8-479D-833C-CAA0950715AA} {FC8AEABA-1A40-4891-9EBA-4B6A1F7244B2} = {21B42F60-5802-404E-90F0-AEBCC56760C0} + {BD2D08FC-8675-4157-A73C-D75F6A3856D3} = {21B42F60-5802-404E-90F0-AEBCC56760C0} EndGlobalSection EndGlobal diff --git a/samples/Sentry.Samples.Console.Metrics/Program.cs b/samples/Sentry.Samples.Console.Metrics/Program.cs new file mode 100644 index 0000000000..f0170ebc7a --- /dev/null +++ b/samples/Sentry.Samples.Console.Metrics/Program.cs @@ -0,0 +1,85 @@ +using System.Numerics; + +namespace Sentry.Samples.Console.Metrics; + +internal static class Program +{ + private static readonly Random Roll = new(); + + private static void Main() + { + // Enable the SDK + using (SentrySdk.Init(options => + { + options.Dsn = + // NOTE: ADD YOUR OWN DSN BELOW so you can see the events in your own Sentry account + "https://eb18e953812b41c3aeb042e666fd3b5c@o447951.ingest.sentry.io/5428537"; + + options.Debug = true; + options.IsGlobalModeEnabled = true; + // Initialize some (non null) ExperimentalMetricsOptions to enable Sentry Metrics, + options.ExperimentalMetrics = new ExperimentalMetricsOptions + { + EnableCodeLocations = + true // Set this to false if you don't want to track code locations for some reason + }; + })) + { + System.Console.WriteLine("Measure, Yeah, Measure"); + PlaySetBingo(30); + CreateRevenueGauge(1000); + MeasureShrimp(1000); + System.Console.WriteLine("Measure up"); + } + } + + private static void PlaySetBingo(int attempts) + { + var solution = new[] { 3, 5, 7, 11, 13, 17 }; + + // The Timing class creates a distribution that is designed to measure the amount of time it takes to run code + // blocks. By default it will use a unit of Seconds - we're configuring it to use milliseconds here though. + using (new Timing(nameof(PlaySetBingo), MeasurementUnit.Duration.Millisecond)) + { + for (var i = 0; i < attempts; i++) + { + var guess = Roll.Next(1, 100); + // This demonstrates the use of a set metric. + SentrySdk.Metrics.Gauge("guesses", guess); + + if (solution.Contains(guess)) + { + // And this is a counter + SentrySdk.Metrics.Increment("correct_answers"); + } + } + } + } + + private static void CreateRevenueGauge(int sampleCount) + { + using (new Timing(nameof(CreateRevenueGauge), MeasurementUnit.Duration.Millisecond)) + { + for (var i = 0; i < sampleCount; i++) + { + var movement = Roll.NextDouble() * 30 - Roll.NextDouble() * 10; + // This demonstrates measuring something in your app using a gauge... we're also using a custom + // measurement unit here (which is optional - by default the unit will be "None") + SentrySdk.Metrics.Gauge("revenue", movement, MeasurementUnit.Custom("$")); + } + } + } + + private static void MeasureShrimp(int sampleCount) + { + using (new Timing(nameof(MeasureShrimp), MeasurementUnit.Duration.Millisecond)) + { + for (var i = 0; i < sampleCount; i++) + { + var sizeOfShrimp = 15 + Roll.NextDouble() * 30; + // This is an example of emitting a distribution metric + SentrySdk.Metrics.Distribution("shrimp.size", sizeOfShrimp, MeasurementUnit.Custom("cm")); + } + } + } +} diff --git a/samples/Sentry.Samples.Console.Metrics/Sentry.Samples.Console.Metrics.csproj b/samples/Sentry.Samples.Console.Metrics/Sentry.Samples.Console.Metrics.csproj new file mode 100644 index 0000000000..beb1ec147b --- /dev/null +++ b/samples/Sentry.Samples.Console.Metrics/Sentry.Samples.Console.Metrics.csproj @@ -0,0 +1,12 @@ + + + + Exe + net8.0 + + + + + + + diff --git a/src/Sentry/DelegatingMetricAggregator.cs b/src/Sentry/DelegatingMetricAggregator.cs index b5cc65dd12..0cee1e4c1b 100644 --- a/src/Sentry/DelegatingMetricAggregator.cs +++ b/src/Sentry/DelegatingMetricAggregator.cs @@ -3,17 +3,17 @@ namespace Sentry; internal class DelegatingMetricAggregator(IMetricAggregator innerAggregator) : IMetricAggregator { public void Increment(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null) => innerAggregator.Increment(key, value, unit, tags, timestamp); + DateTime? timestamp = null, int stackLevel = 0) => innerAggregator.Increment(key, value, unit, tags, timestamp, stackLevel + 1); public void Gauge(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null) => innerAggregator.Gauge(key, value, unit, tags, timestamp); + DateTime? timestamp = null, int stackLevel = 0) => innerAggregator.Gauge(key, value, unit, tags, timestamp, stackLevel + 1); public void Distribution(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null) => innerAggregator.Distribution(key, value, unit, tags, timestamp); + DateTime? timestamp = null, int stackLevel = 0) => innerAggregator.Distribution(key, value, unit, tags, timestamp, stackLevel + 1); public void Set(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null) => innerAggregator.Set(key, value, unit, tags, timestamp); + DateTime? timestamp = null, int stackLevel = 0) => innerAggregator.Set(key, value, unit, tags, timestamp, stackLevel + 1); public void Timing(string key, double value, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, IDictionary? tags = null, - DateTime? timestamp = null) => innerAggregator.Timing(key, value, unit, tags, timestamp); + DateTime? timestamp = null, int stackLevel = 0) => innerAggregator.Timing(key, value, unit, tags, timestamp, stackLevel + 1); } diff --git a/src/Sentry/DisabledMetricAggregator.cs b/src/Sentry/DisabledMetricAggregator.cs index 0c7eff7af3..6fb410dc07 100644 --- a/src/Sentry/DisabledMetricAggregator.cs +++ b/src/Sentry/DisabledMetricAggregator.cs @@ -3,31 +3,31 @@ namespace Sentry; internal class DisabledMetricAggregator : IMetricAggregator { public void Increment(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null) + DateTime? timestamp = null, int stackLevel = 0) { // No Op } public void Gauge(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null) + DateTime? timestamp = null, int stackLevel = 0) { // No Op } public void Distribution(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null) + DateTime? timestamp = null, int stackLevel = 0) { // No Op } public void Set(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null) + DateTime? timestamp = null, int stackLevel = 0) { // No Op } public void Timing(string key, double value, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, IDictionary? tags = null, - DateTime? timestamp = null) + DateTime? timestamp = null, int stackLevel = 0) { // No Op } diff --git a/src/Sentry/IMetricAggregator.cs b/src/Sentry/IMetricAggregator.cs index 796ced57b3..75fa33d488 100644 --- a/src/Sentry/IMetricAggregator.cs +++ b/src/Sentry/IMetricAggregator.cs @@ -16,13 +16,14 @@ public interface IMetricAggregator /// /// The time when the metric was emitted. Defaults to the time at which the metric is emitted, if no value is provided. /// + /// Optional number of stacks levels to ignore when determining the code location void Increment( string key, double value = 1.0, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null - // , int stacklevel = 0 // Used for code locations + DateTime? timestamp = null, + int stackLevel = 0 ); /// @@ -35,13 +36,14 @@ void Increment( /// /// The time when the metric was emitted. Defaults to the time at which the metric is emitted, if no value is provided. /// + /// Optional number of stacks levels to ignore when determining the code location void Gauge( string key, double value = 1.0, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null - // , int stacklevel = 0 // Used for code locations + DateTime? timestamp = null, + int stackLevel = 0 ); /// @@ -54,13 +56,14 @@ void Gauge( /// /// The time when the metric was emitted. Defaults to the time at which the metric is emitted, if no value is provided. /// + /// Optional number of stacks levels to ignore when determining the code location void Distribution( string key, double value = 1.0, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null - // , int stacklevel = 0 // Used for code locations + DateTime? timestamp = null, + int stackLevel = 0 ); /// @@ -73,13 +76,14 @@ void Distribution( /// /// The time when the metric was emitted. Defaults to the time at which the metric is emitted, if no value is provided. /// + /// Optional number of stacks levels to ignore when determining the code location void Set( string key, double value = 1.0, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null - // , int stacklevel = 0 // Used for code locations + DateTime? timestamp = null, + int stackLevel = 0 ); /// @@ -92,12 +96,13 @@ void Set( /// /// Optional Tags to associate with the metric /// The time when the metric was emitted + /// Optional number of stacks levels to ignore when determining the code location void Timing( string key, double value, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, IDictionary? tags = null, - DateTime? timestamp = null - // , int stacklevel = 0 // Used for code locations + DateTime? timestamp = null, + int stackLevel = 0 ); } diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index 92a16216e1..5408e7c44a 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -1,4 +1,5 @@ using Sentry.Extensibility; +using Sentry.Internal; using Sentry.Internal.Extensions; using Sentry.Protocol.Metrics; @@ -6,10 +7,9 @@ namespace Sentry; internal class MetricAggregator : IMetricAggregator, IDisposable { - internal enum MetricType : byte { Counter, Gauge, Distribution, Set } - private readonly SentryOptions _options; private readonly Action> _captureMetrics; + private readonly Action _captureCodeLocations; private readonly TimeSpan _flushInterval; private readonly CancellationTokenSource _shutdownSource; @@ -19,12 +19,12 @@ internal enum MetricType : byte { Counter, Gauge, Distribution, Set } // 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 internal ConcurrentDictionary> Buckets => _buckets.Value; + private readonly Lazy>> _buckets = new(() => new ConcurrentDictionary>()); - // TODO: Initialize seen_locations - // self._seen_locations = _set() # type: Set[Tuple[int, MetricMetaKey]] - // self._pending_locations = {} # type: Dict[int, List[Tuple[MetricMetaKey, Any]]] + private readonly HashSet<(long, MetricResourceIdentifier)> _seenLocations = new(); + private Dictionary> _pendingLocations = new(); private Task LoopTask { get; } @@ -32,17 +32,20 @@ private readonly Lazy /// The - /// The callback to be called to transmit aggregated metrics to a statsd server + /// The callback to be called to transmit aggregated metrics + /// The callback to be called to transmit new code locations /// A /// /// A boolean value indicating whether the Loop to flush metrics should run, for testing only. /// /// An optional flushInterval, for testing only public MetricAggregator(SentryOptions options, Action> captureMetrics, - CancellationTokenSource? shutdownSource = null, bool disableLoopTask = false, TimeSpan? flushInterval = null) + Action captureCodeLocations, CancellationTokenSource? shutdownSource = null, + bool disableLoopTask = false, TimeSpan? flushInterval = null) { _options = options; _captureMetrics = captureMetrics; + _captureCodeLocations = captureCodeLocations; _shutdownSource = shutdownSource ?? new CancellationTokenSource(); _flushInterval = flushInterval ?? TimeSpan.FromSeconds(5); @@ -59,7 +62,8 @@ public MetricAggregator(SentryOptions options, Action> captu } } - internal static string GetMetricBucketKey(MetricType type, string metricKey, MeasurementUnit unit, IDictionary? tags) + internal static string GetMetricBucketKey(MetricType type, string metricKey, MeasurementUnit unit, + IDictionary? tags) { var typePrefix = type switch { @@ -80,9 +84,9 @@ public void Increment( double value = 1.0, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null - // , int stacklevel = 0 // Used for code locations - ) => Emit(MetricType.Counter, key, value, unit, tags, timestamp); + DateTime? timestamp = null, + int stackLevel = 0 + ) => Emit(MetricType.Counter, key, value, unit, tags, timestamp, stackLevel + 1); /// public void Gauge( @@ -90,9 +94,9 @@ public void Gauge( double value = 1.0, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null - // , int stacklevel = 0 // Used for code locations - ) => Emit(MetricType.Gauge, key, value, unit, tags, timestamp); + DateTime? timestamp = null, + int stackLevel = 0 + ) => Emit(MetricType.Gauge, key, value, unit, tags, timestamp, stackLevel + 1); /// public void Distribution( @@ -100,9 +104,9 @@ public void Distribution( double value = 1.0, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null - // , int stacklevel = 0 // Used for code locations - ) => Emit(MetricType.Distribution, key, value, unit, tags, timestamp); + DateTime? timestamp = null, + int stackLevel = 0 + ) => Emit(MetricType.Distribution, key, value, unit, tags, timestamp, stackLevel + 1); /// public void Set( @@ -110,9 +114,9 @@ public void Set( double value = 1.0, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null - // , int stacklevel = 0 // Used for code locations - ) => Emit(MetricType.Set, key, value, unit, tags, timestamp); + DateTime? timestamp = null, + int stackLevel = 0 + ) => Emit(MetricType.Set, key, value, unit, tags, timestamp, stackLevel + 1); /// public void Timing( @@ -120,19 +124,20 @@ public void Timing( double value, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, IDictionary? tags = null, - DateTime? timestamp = null) - // , int stacklevel = 0 // Used for code locations - => Emit(MetricType.Distribution, key, value, unit, tags, timestamp); + DateTime? timestamp = null, + int stackLevel = 0 + ) => Emit(MetricType.Distribution, key, value, unit, tags, timestamp, stackLevel + 1); private readonly object _emitLock = new object(); + private void Emit( MetricType type, string key, double value = 1.0, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null - // , int stacklevel = 0 // Used for code locations + DateTime? timestamp = null, + int stackLevel = 0 ) { timestamp ??= DateTime.UtcNow; @@ -157,47 +162,74 @@ private void Emit( timeBucket.AddOrUpdate( GetMetricBucketKey(type, key, unit.Value, tags), addValuesFactory, - (_, metric) => { + (_, metric) => + { metric.Add(value); return metric; }); } - // TODO: record the code location - // if stacklevel is not None: - // self.record_code_location(ty, key, unit, stacklevel + 2, timestamp) + if (_options.ExperimentalMetrics is { EnableCodeLocations: true }) + { + RecordCodeLocation(type, key, unit.Value, stackLevel + 1, timestamp.Value); + } + } + + private readonly ReaderWriterLockSlim _codeLocationLock = new(); + private void RecordCodeLocation( + MetricType type, + string key, + MeasurementUnit unit, + int stackLevel, + DateTime timestamp + ) + { + var startOfDay = timestamp.GetDayBucketKey(); + var metaKey = new MetricResourceIdentifier(type, key, unit); + + _codeLocationLock.EnterUpgradeableReadLock(); + try + { + if (_seenLocations.Contains((startOfDay, metaKey))) + { + return; + } + _codeLocationLock.EnterWriteLock(); + try + { + // Group metadata by day to make flushing more efficient. + _seenLocations.Add((startOfDay, metaKey)); + if (GetCodeLocation(stackLevel + 1) is not { } location) + { + return; + } + + if (!_pendingLocations.ContainsKey(startOfDay)) + { + _pendingLocations[startOfDay] = new Dictionary(); + } + _pendingLocations[startOfDay][metaKey] = location; + } + finally + { + _codeLocationLock.ExitWriteLock(); + } + } + finally + { + _codeLocationLock.ExitUpgradeableReadLock(); + } } - // TODO: record_code_location - // def record_code_location( - // self, - // ty, # type: MetricType - // key, # type: str - // unit, # type: MeasurementUnit - // stacklevel, # type: int - // timestamp=None, # type: Optional[float] - // ): - // # type: (...) -> None - // if not self._enable_code_locations: - // return - // if timestamp is None: - // timestamp = time.time() - // meta_key = (ty, key, unit) - // start_of_day = utc_from_timestamp(timestamp).replace( - // hour=0, minute=0, second=0, microsecond=0, tzinfo=None - // ) - // start_of_day = int(to_timestamp(start_of_day)) - // - // if (start_of_day, meta_key) not in self._seen_locations: - // self._seen_locations.add((start_of_day, meta_key)) - // loc = get_code_location(stacklevel + 3) - // if loc is not None: - // # Group metadata by day to make flushing more efficient. - // # There needs to be one envelope item per timestamp. - // self._pending_locations.setdefault(start_of_day, []).append( - // (meta_key, loc) - // ) + internal SentryStackFrame? GetCodeLocation(int stackLevel) + { + var stackTrace = new StackTrace(false); + var frames = DebugStackTrace.Create(_options, stackTrace, false).Frames; + return (frames.Count >= stackLevel) + ? frames[^(stackLevel + 1)] + : null; + } private async Task RunLoopAsync() { @@ -239,7 +271,7 @@ private async Task RunLoopAsync() } } - if (shutdownRequested || !Flush()) + if (shutdownRequested || !Flush(false)) { return; } @@ -255,20 +287,20 @@ private async Task RunLoopAsync() private readonly object _flushLock = new(); /// - /// Flushes any flushable buckets. - /// If is true then the cutoff is ignored and all buckets are flushed. + /// Flushes any flushable metrics and/or code locations. + /// If is true then the cutoff is ignored and all metrics are flushed. /// /// Forces all buckets to be flushed, ignoring the cutoff /// False if a shutdown is requested during flush, true otherwise - private bool Flush(bool force = false) + internal bool Flush(bool force = true) { - // We don't want multiple flushes happening concurrently... which might be possible if the regular flush loop - // triggered a flush at the same time ForceFlush is called - lock(_flushLock) + try { - foreach (var key in GetFlushableBuckets(force)) + // We don't want multiple flushes happening concurrently... which might be possible if the regular flush loop + // triggered a flush at the same time ForceFlush is called + lock (_flushLock) { - try + foreach (var key in GetFlushableBuckets(force)) { _options.LogDebug("Flushing metrics for bucket {0}", key); if (Buckets.TryRemove(key, out var bucket)) @@ -277,28 +309,28 @@ private bool Flush(bool force = false) _options.LogDebug("Metric flushed for bucket {0}", key); } } - catch (OperationCanceledException) - { - _options.LogInfo("Shutdown token triggered. Time to exit."); - return false; - } - catch (Exception exception) + + foreach (var (timestamp, locations) in FlushableLocations()) { - _options.LogError(exception, "Error while processing metric aggregates."); + // There needs to be one envelope item per timestamp. + var codeLocations = new CodeLocations(timestamp, locations); + _captureCodeLocations(codeLocations); } } - - // TODO: Flush the code locations - // for timestamp, locations in GetFlushableLocations()): - // encoded_locations = _encode_locations(timestamp, locations) - // envelope.add_item(Item(payload=encoded_locations, type="metric_meta")) + } + catch (OperationCanceledException) + { + _options.LogInfo("Shutdown token triggered. Time to exit."); + return false; + } + catch (Exception exception) + { + _options.LogError(exception, "Error while processing metric aggregates."); } return true; } - internal bool ForceFlush() => Flush(true); - /// /// Returns the keys for any buckets that are ready to be flushed (i.e. are for periods before the cutoff) /// @@ -323,7 +355,7 @@ internal IEnumerable GetFlushableBuckets(bool force = false) } else { - var cutoff = MetricBucketHelper.GetCutoff(); + var cutoff = MetricHelper.GetCutoff(); foreach (var key in Buckets.Keys) { var bucketTime = DateTimeOffset.FromUnixTimeSeconds(key); @@ -335,13 +367,20 @@ internal IEnumerable GetFlushableBuckets(bool force = false) } } - // TODO: _flushable_locations - // def _flushable_locations(self): - // # type: (...) -> Dict[int, List[Tuple[MetricMetaKey, Dict[str, Any]]]] - // with self._lock: - // locations = self._pending_locations - // self._pending_locations = {} - // return locations + Dictionary> FlushableLocations() + { + _codeLocationLock.EnterWriteLock(); + try + { + var result = _pendingLocations; + _pendingLocations = new Dictionary>(); + return result; + } + finally + { + _codeLocationLock.ExitWriteLock(); + } + } /// /// Stops the background worker and waits for it to empty the queue until 'shutdownTimeout' is reached diff --git a/src/Sentry/MetricBucketHelper.cs b/src/Sentry/MetricHelper.cs similarity index 69% rename from src/Sentry/MetricBucketHelper.cs rename to src/Sentry/MetricHelper.cs index 1e9db01175..744de6b832 100644 --- a/src/Sentry/MetricBucketHelper.cs +++ b/src/Sentry/MetricHelper.cs @@ -1,6 +1,6 @@ namespace Sentry; -internal static class MetricBucketHelper +internal static class MetricHelper { private const int RollupInSeconds = 10; @@ -10,6 +10,13 @@ internal static class MetricBucketHelper static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); #endif + internal static long GetDayBucketKey(this DateTime timestamp) + { + var utc = timestamp.ToUniversalTime(); + var dayOnly = new DateTime(utc.Year, utc.Month, utc.Day, 0, 0, 0, 0, DateTimeKind.Utc); + return (long)(dayOnly - UnixEpoch).TotalSeconds; + } + internal static long GetTimeBucketKey(this DateTime timestamp) { var seconds = (long)(timestamp.ToUniversalTime() - UnixEpoch).TotalSeconds; @@ -27,4 +34,7 @@ internal static long GetTimeBucketKey(this DateTime timestamp) internal static DateTime GetCutoff() => DateTime.UtcNow .Subtract(TimeSpan.FromSeconds(RollupInSeconds)) .Subtract(TimeSpan.FromMilliseconds(FlushShift)); + + internal static string SanitizeKey(string input) => Regex.Replace(input, @"[^a-zA-Z0-9_/.-]+", "_"); + internal static string SanitizeValue(string input) => Regex.Replace(input, @"[^\w\d_:/@\.\{\}\[\]$-]+", "_"); } diff --git a/src/Sentry/Protocol/Envelopes/Envelope.cs b/src/Sentry/Protocol/Envelopes/Envelope.cs index 60b292feee..4aa7e7f778 100644 --- a/src/Sentry/Protocol/Envelopes/Envelope.cs +++ b/src/Sentry/Protocol/Envelopes/Envelope.cs @@ -321,6 +321,19 @@ public static Envelope FromTransaction(Transaction transaction) return new Envelope(eventId, header, items); } + /// + /// Creates an envelope that contains one or more + /// + internal static Envelope FromCodeLocations(CodeLocations codeLocations) + { + var header = DefaultHeader; + + List items = new(); + items.Add(EnvelopeItem.FromCodeLocations(codeLocations)); + + return new Envelope(header, items); + } + /// /// Creates an envelope that contains one or more Metrics /// diff --git a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs index 5addcbf89c..08e5e8a825 100644 --- a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs +++ b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs @@ -20,6 +20,7 @@ public sealed class EnvelopeItem : ISerializable, IDisposable private const string TypeValueClientReport = "client_report"; private const string TypeValueProfile = "profile"; private const string TypeValueMetric = "statsd"; + private const string TypeValueCodeLocations = "metric_meta"; private const string LengthKey = "length"; private const string FileNameKey = "filename"; @@ -223,6 +224,20 @@ public static EnvelopeItem FromTransaction(Transaction transaction) return new EnvelopeItem(header, new JsonSerializable(transaction)); } + /// + /// Creates an from one or more . + /// + internal static EnvelopeItem FromCodeLocations(CodeLocations codeLocations) + { + var header = new Dictionary(1, StringComparer.Ordinal) + { + [TypeKey] = TypeValueCodeLocations + }; + + // Note that metrics are serialized using statsd encoding (not JSON) + return new EnvelopeItem(header, new JsonSerializable(codeLocations)); + } + /// /// Creates an from . /// diff --git a/src/Sentry/Protocol/Metrics/CodeLocations.cs b/src/Sentry/Protocol/Metrics/CodeLocations.cs new file mode 100644 index 0000000000..944fb88afd --- /dev/null +++ b/src/Sentry/Protocol/Metrics/CodeLocations.cs @@ -0,0 +1,34 @@ +using Sentry.Extensibility; +using Sentry.Internal.Extensions; + +namespace Sentry.Protocol.Metrics; + +internal class CodeLocations(long timestamp, Dictionary locations) + : IJsonSerializable +{ + /// + /// Uniquely identifies a code location using the number of seconds since the UnixEpoch, as measured at the start + /// of the day when the code location was recorded. + /// + public long Timestamp { get; set; } = timestamp; + + public Dictionary Locations { get; set; } = locations; + + public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) + { + writer.WriteStartObject(); + writer.WriteNumber("timestamp", Timestamp); + + var mapping = Locations.ToDictionary( + kvp => kvp.Key.ToString(), + kvp => + { + var loc = kvp.Value; + loc.IsCodeLocation = true; + return loc; + }); + writer.WriteDictionary("mapping", mapping, logger); + + writer.WriteEndObject(); + } +} diff --git a/src/Sentry/Protocol/Metrics/Metric.cs b/src/Sentry/Protocol/Metrics/Metric.cs index c896195f45..0cc3d05ef9 100644 --- a/src/Sentry/Protocol/Metrics/Metric.cs +++ b/src/Sentry/Protocol/Metrics/Metric.cs @@ -50,15 +50,12 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) protected abstract IEnumerable SerializedStatsdValues(); - internal static string SanitizeKey(string input) => Regex.Replace(input, @"[^a-zA-Z0-9_/.-]+", "_"); - internal static string SanitizeValue(string input) => Regex.Replace(input, @"[^\w\d_:/@\.\{\}\[\]$-]+", "_"); - public async Task SerializeAsync(Stream stream, IDiagnosticLogger? logger, CancellationToken cancellationToken = default) { /* * We're serializing using the statsd format here: https://github.com/b/statsd_spec */ - var metricName = SanitizeKey(Key); + var metricName = MetricHelper.SanitizeKey(Key); await Write($"{metricName}@").ConfigureAwait(false); var unit = Unit ?? MeasurementUnit.None; // We don't need ConfigureAwait(false) here as ConfigureAwait on metricName above avoids capturing the ExecutionContext. @@ -78,7 +75,7 @@ public async Task SerializeAsync(Stream stream, IDiagnosticLogger? logger, Cance var first = true; foreach (var (key, value) in tags) { - var tagKey = SanitizeKey(key); + var tagKey = MetricHelper.SanitizeKey(key); if (string.IsNullOrWhiteSpace(tagKey)) { continue; diff --git a/src/Sentry/Protocol/Metrics/MetricResourceIdentifier.cs b/src/Sentry/Protocol/Metrics/MetricResourceIdentifier.cs new file mode 100644 index 0000000000..b492afe78b --- /dev/null +++ b/src/Sentry/Protocol/Metrics/MetricResourceIdentifier.cs @@ -0,0 +1,6 @@ +namespace Sentry.Protocol.Metrics; + +internal record struct MetricResourceIdentifier(MetricType MetricType, string Key, MeasurementUnit Unit) +{ + public override string ToString() => $"{MetricType}:{MetricHelper.SanitizeKey(Key)}@{Unit}"; +} diff --git a/src/Sentry/Protocol/Metrics/MetricType.cs b/src/Sentry/Protocol/Metrics/MetricType.cs new file mode 100644 index 0000000000..fb0d2cce2b --- /dev/null +++ b/src/Sentry/Protocol/Metrics/MetricType.cs @@ -0,0 +1,3 @@ +namespace Sentry.Protocol.Metrics; + +internal enum MetricType : byte { Counter, Gauge, Distribution, Set } diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index d9c2486bbd..de0af58535 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -75,9 +75,9 @@ internal SentryClient( Worker = worker; } - if (options.ExperimentalMetrics is { MetricSampleRate: > 0 } experimentalMetricsOptions) + if (options.ExperimentalMetrics is not null) { - Metrics = new MetricAggregator(options, CaptureMetrics); + Metrics = new MetricAggregator(options, CaptureMetrics, CaptureCodeLocations); } else { @@ -247,6 +247,15 @@ internal void CaptureMetrics(IEnumerable metrics) CaptureEnvelope(Envelope.FromMetrics(metrics)); } + /// + /// Captures one or more to be sent to Sentry. + /// + internal void CaptureCodeLocations(CodeLocations codeLocations) + { + _options.LogDebug($"Capturing code locations for period: {codeLocations.Timestamp}"); + CaptureEnvelope(Envelope.FromCodeLocations(codeLocations)); + } + /// public void CaptureSession(SessionUpdate sessionUpdate) { @@ -460,5 +469,12 @@ public void Dispose() // Worker should empty it's queue until SentryOptions.ShutdownTimeout Worker.FlushAsync(_options.ShutdownTimeout).GetAwaiter().GetResult(); + + // TODO: Implement async... should probably move Metrics to the Hub and do it in Hub.Dispose as well + if (Metrics is IDisposable disposableMetrics) + { + _options.LogDebug("Flushing MetricsAggregator"); + disposableMetrics.Dispose(); + } } } diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index 6070e93f33..8b6592ac35 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -1255,5 +1255,5 @@ public class ExperimentalMetricsOptions /// Determines the sample rate for metrics. 0.0 means no metrics will be sent (metrics disabled). 1.0 implies all /// metrics will be sent. /// - public double MetricSampleRate { get; set; } = 0; + public bool EnableCodeLocations { get; set; } = true; } diff --git a/src/Sentry/SentryStackFrame.cs b/src/Sentry/SentryStackFrame.cs index f845fcd457..5cc26814a2 100644 --- a/src/Sentry/SentryStackFrame.cs +++ b/src/Sentry/SentryStackFrame.cs @@ -17,6 +17,13 @@ public sealed class SentryStackFrame : IJsonSerializable internal List? InternalFramesOmitted { get; private set; } + /// + /// When serializing a stack frame as part of the Code Location metadata for Metrics, we need to include an + /// additional "type" property in the serialized payload. This flag indicates whether the stack frame is for + /// a code location or not. + /// + internal bool IsCodeLocation { get; set; } = false; + /// /// The relative file path to the call. /// @@ -139,6 +146,12 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) { writer.WriteStartObject(); + if (IsCodeLocation) + { + // See https://develop.sentry.dev/sdk/metrics/#meta-data + writer.WriteString("type", "location"); + } + writer.WriteStringArrayIfNotEmpty("pre_context", InternalPreContext); writer.WriteStringArrayIfNotEmpty("post_context", InternalPostContext); writer.WriteStringDictionaryIfNotEmpty("vars", InternalVars!); diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt index bf6ca2b347..bf21fa4940 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt @@ -129,7 +129,7 @@ namespace Sentry public class ExperimentalMetricsOptions { public ExperimentalMetricsOptions() { } - public double MetricSampleRate { get; set; } + public bool EnableCodeLocations { get; set; } } public class FileAttachmentContent : Sentry.IAttachmentContent { @@ -244,11 +244,11 @@ namespace Sentry } public interface IMetricAggregator { - void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); - void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); - void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); - void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); - void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); + void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); } public interface IScopeObserver { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt index bf6ca2b347..bf21fa4940 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt @@ -129,7 +129,7 @@ namespace Sentry public class ExperimentalMetricsOptions { public ExperimentalMetricsOptions() { } - public double MetricSampleRate { get; set; } + public bool EnableCodeLocations { get; set; } } public class FileAttachmentContent : Sentry.IAttachmentContent { @@ -244,11 +244,11 @@ namespace Sentry } public interface IMetricAggregator { - void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); - void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); - void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); - void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); - void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); + void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); } public interface IScopeObserver { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 3478076751..68ee6d2a0f 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -130,7 +130,7 @@ namespace Sentry public class ExperimentalMetricsOptions { public ExperimentalMetricsOptions() { } - public double MetricSampleRate { get; set; } + public bool EnableCodeLocations { get; set; } } public class FileAttachmentContent : Sentry.IAttachmentContent { @@ -245,11 +245,11 @@ namespace Sentry } public interface IMetricAggregator { - void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); - void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); - void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); - void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); - void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); + void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); } public interface IScopeObserver { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index a06f0caed7..893b664e5e 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -128,7 +128,7 @@ namespace Sentry public class ExperimentalMetricsOptions { public ExperimentalMetricsOptions() { } - public double MetricSampleRate { get; set; } + public bool EnableCodeLocations { get; set; } } public class FileAttachmentContent : Sentry.IAttachmentContent { @@ -243,11 +243,11 @@ namespace Sentry } public interface IMetricAggregator { - void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); - void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); - void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); - void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); - void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default); + void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); } public interface IScopeObserver { diff --git a/test/Sentry.Tests/MetricAggregatorTests.cs b/test/Sentry.Tests/MetricAggregatorTests.cs index eace7518bb..2bce1142bb 100644 --- a/test/Sentry.Tests/MetricAggregatorTests.cs +++ b/test/Sentry.Tests/MetricAggregatorTests.cs @@ -8,10 +8,11 @@ class Fixture { public SentryOptions Options { get; set; } = new(); public Action> CaptureMetrics { get; set; } = (_ => { }); + public Action CaptureCodeLocations { get; set; } = (_ => { }); public bool DisableFlushLoop { get; set; } = true; public TimeSpan? FlushInterval { get; set; } public MetricAggregator GetSut() - => new(Options, CaptureMetrics, disableLoopTask: DisableFlushLoop, flushInterval: FlushInterval); + => new(Options, CaptureMetrics, CaptureCodeLocations, disableLoopTask: DisableFlushLoop, flushInterval: FlushInterval); } // private readonly Fixture _fixture = new(); @@ -21,7 +22,7 @@ public MetricAggregator GetSut() public void GetMetricBucketKey_GeneratesExpectedKey() { // Arrange - var type = MetricAggregator.MetricType.Counter; + var type = MetricType.Counter; var metricKey = "quibbles"; var unit = MeasurementUnit.None; var tags = new Dictionary { ["tag1"] = "value1" }; @@ -37,7 +38,7 @@ public void GetMetricBucketKey_GeneratesExpectedKey() public void Increment_AggregatesMetrics() { // Arrange - var metricType = MetricAggregator.MetricType.Counter; + var metricType = MetricType.Counter; var key = "counter_key"; var unit = MeasurementUnit.None; var tags = new Dictionary { ["tag1"] = "value1" }; @@ -67,7 +68,7 @@ public void Increment_AggregatesMetrics() public void Gauge_AggregatesMetrics() { // Arrange - var metricType = MetricAggregator.MetricType.Gauge; + var metricType = MetricType.Gauge; var key = "gauge_key"; var unit = MeasurementUnit.None; var tags = new Dictionary { ["tag1"] = "value1" }; @@ -107,7 +108,7 @@ public void Gauge_AggregatesMetrics() public void Distribution_AggregatesMetrics() { // Arrange - var metricType = MetricAggregator.MetricType.Distribution; + var metricType = MetricType.Distribution; var key = "distribution_key"; var unit = MeasurementUnit.None; var tags = new Dictionary { ["tag1"] = "value1" }; @@ -137,7 +138,7 @@ public void Distribution_AggregatesMetrics() public void Set_AggregatesMetrics() { // Arrange - var metricType = MetricAggregator.MetricType.Set; + var metricType = MetricType.Set; var key = "set_key"; var unit = MeasurementUnit.None; var tags = new Dictionary { ["tag1"] = "value1" }; @@ -173,7 +174,7 @@ public void GetFlushableBuckets_IsThreadsafe() const int numThreads = 100; const int numThreadIterations = 1000; var sent = 0; - MetricBucketHelper.FlushShift = 0.0; + MetricHelper.FlushShift = 0.0; _fixture.DisableFlushLoop = false; _fixture.FlushInterval = TimeSpan.FromMilliseconds(100); _fixture.CaptureMetrics = metrics => @@ -206,9 +207,23 @@ public void GetFlushableBuckets_IsThreadsafe() // Wait for workers. resetEvent.WaitOne(); - sut.ForceFlush(); + sut.Flush(); // Assert sent.Should().Be(numThreads * numThreadIterations); } + + [Fact] + public void TestGetCodeLocation() { + // Arrange + _fixture.Options.StackTraceMode = StackTraceMode.Enhanced; + var sut = _fixture.GetSut(); + + // Act + var result = sut.GetCodeLocation(1); + + // Assert + result.Should().NotBeNull(); + result!.Function.Should().Be($"void {nameof(MetricAggregatorTests)}.{nameof(TestGetCodeLocation)}()"); + } } diff --git a/test/Sentry.Tests/MetricBucketHelperTests.cs b/test/Sentry.Tests/MetricBucketHelperTests.cs index c49af2fc60..1854987368 100644 --- a/test/Sentry.Tests/MetricBucketHelperTests.cs +++ b/test/Sentry.Tests/MetricBucketHelperTests.cs @@ -21,4 +21,20 @@ public void GetTimeBucketKey_RoundsDownToNearestTenSeconds(int seconds) // Assert result.Should().Be(3690); // (1 hour) + (1 minute) plus (30 seconds) = 3690 } + + [Theory] + [InlineData(1970, 1, 1, 12, 34, 56, 0)] + [InlineData(1970, 1, 2, 12, 34, 56, 1)] + public void GetDayBucketKey_RoundsStartOfDay(int year, int month, int day, int hour, int minute, int second, int expectedDays) + { + // Arrange + var timestamp = new DateTime(year, month, day, hour, minute, second, DateTimeKind.Utc); + + // Act + var result = timestamp.GetDayBucketKey(); + + // Assert + const int secondsInADay = 60 * 60 * 24; + result.Should().Be(expectedDays * secondsInADay); + } } From 7f47f1a4906a5f08b45ef08035a3eea470f6ec3d Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 14 Dec 2023 16:40:31 +1300 Subject: [PATCH 24/52] Update Program.cs --- samples/Sentry.Samples.AspNetCore.Basic/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/Sentry.Samples.AspNetCore.Basic/Program.cs b/samples/Sentry.Samples.AspNetCore.Basic/Program.cs index 5bfdd48f60..e60576469a 100644 --- a/samples/Sentry.Samples.AspNetCore.Basic/Program.cs +++ b/samples/Sentry.Samples.AspNetCore.Basic/Program.cs @@ -20,7 +20,7 @@ public static IWebHost BuildWebHost(string[] args) => // Enable Sentry performance monitoring o.EnableTracing = true; - o.ExperimentalMetrics = new ExperimentalMetricsOptions(){ MetricSampleRate = 1.0 }; + o.ExperimentalMetrics = new ExperimentalMetricsOptions(); #if DEBUG // Log debug information about the Sentry SDK o.Debug = true; From cd83a4e0117df65b6edee5aa7c4943978af03e09 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 14 Dec 2023 17:21:42 +1300 Subject: [PATCH 25/52] Updated solution filters --- Sentry-CI-Build-Linux.slnf | 1 + Sentry-CI-Build-Windows.slnf | 1 + Sentry-CI-Build-macOS.slnf | 8 ++++---- Sentry.NoMobile.sln | 7 +++++++ SentryNoMobile.slnf | 1 + 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Sentry-CI-Build-Linux.slnf b/Sentry-CI-Build-Linux.slnf index f48a6c3ca8..ea8698b8d9 100644 --- a/Sentry-CI-Build-Linux.slnf +++ b/Sentry-CI-Build-Linux.slnf @@ -14,6 +14,7 @@ "samples\\Sentry.Samples.Azure.Functions.Worker\\Sentry.Samples.Azure.Functions.Worker.csproj", "samples\\Sentry.Samples.Console.Basic\\Sentry.Samples.Console.Basic.csproj", "samples\\Sentry.Samples.Console.Customized\\Sentry.Samples.Console.Customized.csproj", + "samples\\Sentry.Samples.Console.Metrics\\Sentry.Samples.Console.Metrics.csproj", "samples\\Sentry.Samples.Console.Native\\Sentry.Samples.Console.Native.csproj", "samples\\Sentry.Samples.Console.Profiling\\Sentry.Samples.Console.Profiling.csproj", "samples\\Sentry.Samples.EntityFramework\\Sentry.Samples.EntityFramework.csproj", diff --git a/Sentry-CI-Build-Windows.slnf b/Sentry-CI-Build-Windows.slnf index e08b88d7f9..4d8b52312c 100644 --- a/Sentry-CI-Build-Windows.slnf +++ b/Sentry-CI-Build-Windows.slnf @@ -13,6 +13,7 @@ "samples\\Sentry.Samples.Azure.Functions.Worker\\Sentry.Samples.Azure.Functions.Worker.csproj", "samples\\Sentry.Samples.Console.Basic\\Sentry.Samples.Console.Basic.csproj", "samples\\Sentry.Samples.Console.Customized\\Sentry.Samples.Console.Customized.csproj", + "samples\\Sentry.Samples.Console.Metrics\\Sentry.Samples.Console.Metrics.csproj", "samples\\Sentry.Samples.Console.Native\\Sentry.Samples.Console.Native.csproj", "samples\\Sentry.Samples.Console.Profiling\\Sentry.Samples.Console.Profiling.csproj", "samples\\Sentry.Samples.EntityFramework\\Sentry.Samples.EntityFramework.csproj", diff --git a/Sentry-CI-Build-macOS.slnf b/Sentry-CI-Build-macOS.slnf index 248ad15d66..3bb3a387d6 100644 --- a/Sentry-CI-Build-macOS.slnf +++ b/Sentry-CI-Build-macOS.slnf @@ -25,17 +25,17 @@ "samples\\Sentry.Samples.GraphQL.Server\\Sentry.Samples.GraphQL.Server.csproj", "samples\\Sentry.Samples.Ios\\Sentry.Samples.Ios.csproj", "samples\\Sentry.Samples.Log4Net\\Sentry.Samples.Log4Net.csproj", - "samples\\Sentry.Samples.ME.Logging\\Sentry.Samples.ME.Logging.csproj", "samples\\Sentry.Samples.MacCatalyst\\Sentry.Samples.MacCatalyst.csproj", "samples\\Sentry.Samples.Maui\\Sentry.Samples.Maui.csproj", + "samples\\Sentry.Samples.ME.Logging\\Sentry.Samples.ME.Logging.csproj", "samples\\Sentry.Samples.NLog\\Sentry.Samples.NLog.csproj", "samples\\Sentry.Samples.OpenTelemetry.AspNetCore\\Sentry.Samples.OpenTelemetry.AspNetCore.csproj", "samples\\Sentry.Samples.OpenTelemetry.Console\\Sentry.Samples.OpenTelemetry.Console.csproj", "samples\\Sentry.Samples.Serilog\\Sentry.Samples.Serilog.csproj", "src\\Sentry.Android.AssemblyReader\\Sentry.Android.AssemblyReader.csproj", + "src\\Sentry.AspNet\\Sentry.AspNet.csproj", "src\\Sentry.AspNetCore.Grpc\\Sentry.AspNetCore.Grpc.csproj", "src\\Sentry.AspNetCore\\Sentry.AspNetCore.csproj", - "src\\Sentry.AspNet\\Sentry.AspNet.csproj", "src\\Sentry.Azure.Functions.Worker\\Sentry.Azure.Functions.Worker.csproj", "src\\Sentry.Bindings.Android\\Sentry.Bindings.Android.csproj", "src\\Sentry.Bindings.Cocoa\\Sentry.Bindings.Cocoa.csproj", @@ -53,8 +53,8 @@ "test\\Sentry.Android.AssemblyReader.Tests\\Sentry.Android.AssemblyReader.Tests.csproj", "test\\Sentry.AspNet.Tests\\Sentry.AspNet.Tests.csproj", "test\\Sentry.AspNetCore.Grpc.Tests\\Sentry.AspNetCore.Grpc.Tests.csproj", - "test\\Sentry.AspNetCore.TestUtils\\Sentry.AspNetCore.TestUtils.csproj", "test\\Sentry.AspNetCore.Tests\\Sentry.AspNetCore.Tests.csproj", + "test\\Sentry.AspNetCore.TestUtils\\Sentry.AspNetCore.TestUtils.csproj", "test\\Sentry.Azure.Functions.Worker.Tests\\Sentry.Azure.Functions.Worker.Tests.csproj", "test\\Sentry.DiagnosticSource.IntegrationTests\\Sentry.DiagnosticSource.IntegrationTests.csproj", "test\\Sentry.DiagnosticSource.Tests\\Sentry.DiagnosticSource.Tests.csproj", @@ -73,4 +73,4 @@ "test\\SingleFileTestApp\\SingleFileTestApp.csproj" ] } -} \ No newline at end of file +} diff --git a/Sentry.NoMobile.sln b/Sentry.NoMobile.sln index ac60374e4b..3b32ad1694 100644 --- a/Sentry.NoMobile.sln +++ b/Sentry.NoMobile.sln @@ -163,6 +163,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FastSerialization", "module EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Samples.Console.Native", "samples\Sentry.Samples.Console.Native\Sentry.Samples.Console.Native.csproj", "{FC8AEABA-1A40-4891-9EBA-4B6A1F7244B2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Samples.Console.Metrics", "samples\Sentry.Samples.Console.Metrics\Sentry.Samples.Console.Metrics.csproj", "{BD2D08FC-8675-4157-A73C-D75F6A3856D3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -465,6 +467,10 @@ Global {FC8AEABA-1A40-4891-9EBA-4B6A1F7244B2}.Debug|Any CPU.Build.0 = Debug|Any CPU {FC8AEABA-1A40-4891-9EBA-4B6A1F7244B2}.Release|Any CPU.ActiveCfg = Release|Any CPU {FC8AEABA-1A40-4891-9EBA-4B6A1F7244B2}.Release|Any CPU.Build.0 = Release|Any CPU + {BD2D08FC-8675-4157-A73C-D75F6A3856D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BD2D08FC-8675-4157-A73C-D75F6A3856D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BD2D08FC-8675-4157-A73C-D75F6A3856D3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BD2D08FC-8675-4157-A73C-D75F6A3856D3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -545,5 +551,6 @@ Global {67269916-C417-4CEE-BD7D-CA66C3830AEE} = {A3CCA27E-4DF8-479D-833C-CAA0950715AA} {8032310D-3C06-442C-A318-F365BCC4C804} = {A3CCA27E-4DF8-479D-833C-CAA0950715AA} {FC8AEABA-1A40-4891-9EBA-4B6A1F7244B2} = {21B42F60-5802-404E-90F0-AEBCC56760C0} + {BD2D08FC-8675-4157-A73C-D75F6A3856D3} = {21B42F60-5802-404E-90F0-AEBCC56760C0} EndGlobalSection EndGlobal diff --git a/SentryNoMobile.slnf b/SentryNoMobile.slnf index effba4f145..3932f9c12f 100644 --- a/SentryNoMobile.slnf +++ b/SentryNoMobile.slnf @@ -13,6 +13,7 @@ "samples\\Sentry.Samples.Azure.Functions.Worker\\Sentry.Samples.Azure.Functions.Worker.csproj", "samples\\Sentry.Samples.Console.Basic\\Sentry.Samples.Console.Basic.csproj", "samples\\Sentry.Samples.Console.Customized\\Sentry.Samples.Console.Customized.csproj", + "samples\\Sentry.Samples.Console.Metrics\\Sentry.Samples.Console.Metrics.csproj", "samples\\Sentry.Samples.Console.Native\\Sentry.Samples.Console.Native.csproj", "samples\\Sentry.Samples.Console.Profiling\\Sentry.Samples.Console.Profiling.csproj", "samples\\Sentry.Samples.EntityFramework\\Sentry.Samples.EntityFramework.csproj", From 826a41321aa00aa746abf053a5154fc8879dd54e Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 14 Dec 2023 17:38:35 +1300 Subject: [PATCH 26/52] Update CHANGELOG.md --- CHANGELOG.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c86ff9b71b..b054e3339e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Experimental pre-release availability of Delightful Developer Metrics. We're exploring the use of Metrics in Sentry. The API will very likely change and we don't yet have any documentation. ([#2949](https://github.com/getsentry/sentry-dotnet/pull/2949)) + ## 4.0.0-beta.6 ### Feature @@ -14,10 +20,6 @@ - iOS profiling support (alpha). ([#2930](https://github.com/getsentry/sentry-dotnet/pull/2930)) -### Features - -- Experimental pre-release availability of Delightful Developer Metrics. We're exploring the use of Metrics in Sentry. The API will very likely change and we don't yet have any documentation. ([#2949](https://github.com/getsentry/sentry-dotnet/pull/2949)) - ### Fixes - Stop Sentry for MacCatalyst from creating `default.profraw` in the app bundle using xcodebuild archive to build sentry-cocoa ([#2960](https://github.com/getsentry/sentry-dotnet/pull/2960)) From 994b8ba6799dfcfe27d343055e132b3212ec2ab1 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 14 Dec 2023 21:20:55 +1300 Subject: [PATCH 27/52] Changed Flush to FlushAsync --- src/Sentry/MetricAggregator.cs | 120 +++++++++++---------- test/Sentry.Tests/MetricAggregatorTests.cs | 4 +- 2 files changed, 66 insertions(+), 58 deletions(-) diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index 5408e7c44a..caa9db4be1 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -5,7 +5,7 @@ namespace Sentry; -internal class MetricAggregator : IMetricAggregator, IDisposable +internal class MetricAggregator : IMetricAggregator, IDisposable, IAsyncDisposable { private readonly SentryOptions _options; private readonly Action> _captureMetrics; @@ -243,35 +243,34 @@ private async Task RunLoopAsync() while (!shutdownTimeout.IsCancellationRequested) { // If the cancellation was signaled, run until the end of the queue or shutdownTimeout - if (!shutdownRequested) + try { - try - { - await Task.Delay(_flushInterval, shutdownTimeout.Token).ConfigureAwait(false); - } - // Cancellation requested and no timeout allowed, so exit even if there are more items - catch (OperationCanceledException) when (_options.ShutdownTimeout == TimeSpan.Zero) - { - _options.LogDebug("Exiting immediately due to 0 shutdown timeout."); - - shutdownTimeout.Cancel(); - - return; - } - // Cancellation requested, scheduled shutdown - catch (OperationCanceledException) - { - _options.LogDebug( - "Shutdown scheduled. Stopping by: {0}.", - _options.ShutdownTimeout); - - shutdownTimeout.CancelAfterSafe(_options.ShutdownTimeout); - - shutdownRequested = true; - } + await Task.Delay(_flushInterval, _shutdownSource.Token).ConfigureAwait(false); } + // Cancellation requested and no timeout allowed, so exit even if there are more items + catch (OperationCanceledException) when (_options.ShutdownTimeout == TimeSpan.Zero) + { + _options.LogDebug("Exiting immediately due to 0 shutdown timeout."); + + await shutdownTimeout.CancelAsync().ConfigureAwait(false); + + return; + } + // Cancellation requested, scheduled shutdown + catch (OperationCanceledException) + { + _options.LogDebug( + "Shutdown scheduled. Stopping by: {0}.", + _options.ShutdownTimeout); + + shutdownTimeout.CancelAfterSafe(_options.ShutdownTimeout); - if (shutdownRequested || !Flush(false)) + shutdownRequested = true; + } + + await FlushAsync(false, shutdownTimeout.Token).ConfigureAwait(false); + + if (shutdownRequested) { return; } @@ -284,51 +283,59 @@ private async Task RunLoopAsync() } } - private readonly object _flushLock = new(); + private readonly SemaphoreSlim _flushLock = new(1, 1); /// /// Flushes any flushable metrics and/or code locations. /// If is true then the cutoff is ignored and all metrics are flushed. /// /// Forces all buckets to be flushed, ignoring the cutoff + /// A /// False if a shutdown is requested during flush, true otherwise - internal bool Flush(bool force = true) + internal async Task FlushAsync(bool force = true, CancellationToken cancellationToken = default) { try { - // We don't want multiple flushes happening concurrently... which might be possible if the regular flush loop - // triggered a flush at the same time ForceFlush is called - lock (_flushLock) + await _flushLock.WaitAsync(cancellationToken).ConfigureAwait(false); + + foreach (var key in GetFlushableBuckets(force)) { - foreach (var key in GetFlushableBuckets(force)) - { - _options.LogDebug("Flushing metrics for bucket {0}", key); - if (Buckets.TryRemove(key, out var bucket)) - { - _captureMetrics(bucket.Values); - _options.LogDebug("Metric flushed for bucket {0}", key); - } - } + cancellationToken.ThrowIfCancellationRequested(); - foreach (var (timestamp, locations) in FlushableLocations()) + _options.LogDebug("Flushing metrics for bucket {0}", key); + if (!Buckets.TryRemove(key, out var bucket)) { - // There needs to be one envelope item per timestamp. - var codeLocations = new CodeLocations(timestamp, locations); - _captureCodeLocations(codeLocations); + continue; } + + _captureMetrics(bucket.Values); + _options.LogDebug("Metric flushed for bucket {0}", key); + } + + foreach (var (timestamp, locations) in FlushableLocations()) + { + cancellationToken.ThrowIfCancellationRequested(); + + _options.LogDebug("Flushing code locations: ", timestamp); + var codeLocations = new CodeLocations(timestamp, locations); + _captureCodeLocations(codeLocations); + _options.LogDebug("Code locations flushed: ", timestamp); } } catch (OperationCanceledException) { - _options.LogInfo("Shutdown token triggered. Time to exit."); - return false; + _options.LogInfo("Shutdown token triggered. Exiting metric aggregator."); + + return; } catch (Exception exception) { - _options.LogError(exception, "Error while processing metric aggregates."); + _options.LogError(exception, "Error processing metrics."); + } + finally + { + _flushLock.Release(); } - - return true; } /// @@ -367,7 +374,7 @@ internal IEnumerable GetFlushableBuckets(bool force = false) } } - Dictionary> FlushableLocations() + private Dictionary> FlushableLocations() { _codeLocationLock.EnterWriteLock(); try @@ -382,11 +389,8 @@ Dictionary> Flushab } } - /// - /// Stops the background worker and waits for it to empty the queue until 'shutdownTimeout' is reached - /// /// - public void Dispose() + public async ValueTask DisposeAsync() { _options.LogDebug("Disposing MetricAggregator."); @@ -401,7 +405,7 @@ public void Dispose() try { // Request the LoopTask stop. - _shutdownSource.Cancel(); + await _shutdownSource.CancelAsync().ConfigureAwait(false); // Now wait for the Loop to stop. // NOTE: While non-intuitive, do not pass a timeout or cancellation token here. We are waiting for @@ -419,8 +423,12 @@ public void Dispose() } finally { + _flushLock.Dispose(); _shutdownSource.Dispose(); LoopTask.Dispose(); } } + + /// + public void Dispose() => DisposeAsync().GetAwaiter().GetResult(); } diff --git a/test/Sentry.Tests/MetricAggregatorTests.cs b/test/Sentry.Tests/MetricAggregatorTests.cs index 2bce1142bb..ade162deff 100644 --- a/test/Sentry.Tests/MetricAggregatorTests.cs +++ b/test/Sentry.Tests/MetricAggregatorTests.cs @@ -168,7 +168,7 @@ public void Set_AggregatesMetrics() } [Fact] - public void GetFlushableBuckets_IsThreadsafe() + public async Task GetFlushableBuckets_IsThreadsafe() { // Arrange const int numThreads = 100; @@ -207,7 +207,7 @@ public void GetFlushableBuckets_IsThreadsafe() // Wait for workers. resetEvent.WaitOne(); - sut.Flush(); + await sut.FlushAsync(); // Assert sent.Should().Be(numThreads * numThreadIterations); From 8e7f0ce5baa655d9c641919cb9f84d19efb98de8 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 14 Dec 2023 22:40:18 +1300 Subject: [PATCH 28/52] Metrics now get flushed properly when disposing of the Hub --- .../Program.cs | 6 ---- src/Sentry/DelegatingMetricAggregator.cs | 5 ++++ src/Sentry/DisabledMetricAggregator.cs | 12 ++++++++ src/Sentry/IMetricAggregator.cs | 11 +++++++- src/Sentry/Internal/Hub.cs | 6 +++- src/Sentry/MetricAggregator.cs | 28 ++++--------------- .../Metrics/MetricResourceIdentifier.cs | 3 +- src/Sentry/Protocol/Metrics/MetricType.cs | 13 +++++++++ src/Sentry/SentryClient.cs | 24 ++++++++-------- ...piApprovalTests.Run.DotNet6_0.verified.txt | 6 ++-- ...piApprovalTests.Run.DotNet7_0.verified.txt | 6 ++-- ...piApprovalTests.Run.DotNet8_0.verified.txt | 6 ++-- .../ApiApprovalTests.Run.Net4_8.verified.txt | 6 ++-- 13 files changed, 79 insertions(+), 53 deletions(-) diff --git a/samples/Sentry.Samples.AspNetCore.Basic/Program.cs b/samples/Sentry.Samples.AspNetCore.Basic/Program.cs index e60576469a..b55e432a51 100644 --- a/samples/Sentry.Samples.AspNetCore.Basic/Program.cs +++ b/samples/Sentry.Samples.AspNetCore.Basic/Program.cs @@ -20,7 +20,6 @@ public static IWebHost BuildWebHost(string[] args) => // Enable Sentry performance monitoring o.EnableTracing = true; - o.ExperimentalMetrics = new ExperimentalMetricsOptions(); #if DEBUG // Log debug information about the Sentry SDK o.Debug = true; @@ -36,11 +35,6 @@ public static IWebHost BuildWebHost(string[] args) => // exception when serving a request to path: /throw app.UseEndpoints(endpoints => { - endpoints.MapGet("/hello", () => - { - SentrySdk.Metrics.Increment("hello.world"); - return "Hello World!"; - }); // Reported events will be grouped by route pattern endpoints.MapGet("/throw/{message?}", context => { diff --git a/src/Sentry/DelegatingMetricAggregator.cs b/src/Sentry/DelegatingMetricAggregator.cs index 0cee1e4c1b..58904039e3 100644 --- a/src/Sentry/DelegatingMetricAggregator.cs +++ b/src/Sentry/DelegatingMetricAggregator.cs @@ -16,4 +16,9 @@ public void Set(string key, double value = 1, MeasurementUnit? unit = null, IDic public void Timing(string key, double value, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, IDictionary? tags = null, DateTime? timestamp = null, int stackLevel = 0) => innerAggregator.Timing(key, value, unit, tags, timestamp, stackLevel + 1); + + public Task FlushAsync(bool force = true, CancellationToken cancellationToken = default) => + innerAggregator.FlushAsync(force, cancellationToken); + + public ValueTask DisposeAsync() => innerAggregator.DisposeAsync(); } diff --git a/src/Sentry/DisabledMetricAggregator.cs b/src/Sentry/DisabledMetricAggregator.cs index 6fb410dc07..b52ebe9603 100644 --- a/src/Sentry/DisabledMetricAggregator.cs +++ b/src/Sentry/DisabledMetricAggregator.cs @@ -31,4 +31,16 @@ public void Timing(string key, double value, MeasurementUnit.Duration unit = Mea { // No Op } + + public Task FlushAsync(bool force = true, CancellationToken cancellationToken = default) + { + // No Op + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() + { + // No Op + return default; + } } diff --git a/src/Sentry/IMetricAggregator.cs b/src/Sentry/IMetricAggregator.cs index 75fa33d488..597eb7870a 100644 --- a/src/Sentry/IMetricAggregator.cs +++ b/src/Sentry/IMetricAggregator.cs @@ -4,7 +4,7 @@ namespace Sentry; /// Exposes EXPERIMENTAL capability to emit metrics. This API is subject to change without major version bumps so use /// with caution. We advise disabling in production at the moment. /// -public interface IMetricAggregator +public interface IMetricAggregator: IAsyncDisposable { /// /// Emits a Counter metric @@ -105,4 +105,13 @@ void Timing( DateTime? timestamp = null, int stackLevel = 0 ); + + /// + /// Flushes any flushable metrics and/or code locations. + /// If is true then the cutoff is ignored and all metrics are flushed. + /// + /// Forces all buckets to be flushed, ignoring the cutoff + /// A + /// False if a shutdown is requested during flush, true otherwise + Task FlushAsync(bool force = true, CancellationToken cancellationToken = default); } diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 0267dc2747..ec4d335329 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -525,7 +525,11 @@ public void Dispose() return; } - _ownedClient.Flush(_options.ShutdownTimeout); + var disposeTasks = new List { + _ownedClient.Metrics.FlushAsync(), + _ownedClient.FlushAsync(_options.ShutdownTimeout) + }; + Task.WhenAll(disposeTasks).GetAwaiter().GetResult(); //Dont dispose of ScopeManager since we want dangling transactions to still be able to access tags. #if __IOS__ diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index caa9db4be1..c93db6c140 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -5,7 +5,7 @@ namespace Sentry; -internal class MetricAggregator : IMetricAggregator, IDisposable, IAsyncDisposable +internal class MetricAggregator : IMetricAggregator { private readonly SentryOptions _options; private readonly Action> _captureMetrics; @@ -65,14 +65,7 @@ public MetricAggregator(SentryOptions options, Action> captu internal static string GetMetricBucketKey(MetricType type, string metricKey, MeasurementUnit unit, IDictionary? tags) { - var typePrefix = type switch - { - MetricType.Counter => "c", - MetricType.Gauge => "g", - MetricType.Distribution => "d", - MetricType.Set => "s", - _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) - }; + var typePrefix = type.ToStatsdType(); var serializedTags = tags?.ToUtf8Json() ?? string.Empty; return $"{typePrefix}_{metricKey}_{unit}_{serializedTags}"; @@ -268,7 +261,7 @@ private async Task RunLoopAsync() shutdownRequested = true; } - await FlushAsync(false, shutdownTimeout.Token).ConfigureAwait(false); + await FlushAsync(shutdownRequested, shutdownTimeout.Token).ConfigureAwait(false); if (shutdownRequested) { @@ -285,14 +278,8 @@ private async Task RunLoopAsync() private readonly SemaphoreSlim _flushLock = new(1, 1); - /// - /// Flushes any flushable metrics and/or code locations. - /// If is true then the cutoff is ignored and all metrics are flushed. - /// - /// Forces all buckets to be flushed, ignoring the cutoff - /// A - /// False if a shutdown is requested during flush, true otherwise - internal async Task FlushAsync(bool force = true, CancellationToken cancellationToken = default) + /// + public async Task FlushAsync(bool force = true, CancellationToken cancellationToken = default) { try { @@ -325,8 +312,6 @@ internal async Task FlushAsync(bool force = true, CancellationToken cancellation catch (OperationCanceledException) { _options.LogInfo("Shutdown token triggered. Exiting metric aggregator."); - - return; } catch (Exception exception) { @@ -428,7 +413,4 @@ public async ValueTask DisposeAsync() LoopTask.Dispose(); } } - - /// - public void Dispose() => DisposeAsync().GetAwaiter().GetResult(); } diff --git a/src/Sentry/Protocol/Metrics/MetricResourceIdentifier.cs b/src/Sentry/Protocol/Metrics/MetricResourceIdentifier.cs index b492afe78b..3cd3a3906e 100644 --- a/src/Sentry/Protocol/Metrics/MetricResourceIdentifier.cs +++ b/src/Sentry/Protocol/Metrics/MetricResourceIdentifier.cs @@ -2,5 +2,6 @@ namespace Sentry.Protocol.Metrics; internal record struct MetricResourceIdentifier(MetricType MetricType, string Key, MeasurementUnit Unit) { - public override string ToString() => $"{MetricType}:{MetricHelper.SanitizeKey(Key)}@{Unit}"; + public override string ToString() + => $"{MetricType.ToStatsdType()}:{MetricHelper.SanitizeKey(Key)}@{Unit}"; } diff --git a/src/Sentry/Protocol/Metrics/MetricType.cs b/src/Sentry/Protocol/Metrics/MetricType.cs index fb0d2cce2b..c323b914dd 100644 --- a/src/Sentry/Protocol/Metrics/MetricType.cs +++ b/src/Sentry/Protocol/Metrics/MetricType.cs @@ -1,3 +1,16 @@ namespace Sentry.Protocol.Metrics; internal enum MetricType : byte { Counter, Gauge, Distribution, Set } + +internal static class MetricTypeExtensions +{ + internal static string ToStatsdType(this MetricType type) => + type switch + { + MetricType.Counter => "c", + MetricType.Gauge => "g", + MetricType.Distribution => "d", + MetricType.Set => "s", + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; +} diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index 4ffdb33fd0..fde60527fe 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -14,7 +14,7 @@ namespace Sentry; /// /// /// -public class SentryClient : ISentryClient, IDisposable +public class SentryClient : ISentryClient, IDisposable, IAsyncDisposable { private readonly SentryOptions _options; private readonly ISessionManager _sessionManager; @@ -462,22 +462,20 @@ private bool CaptureEnvelope(Envelope envelope) return @event; } - /// - /// Disposes this client - /// /// - public void Dispose() + public async ValueTask DisposeAsync() { _options.LogDebug("Flushing SentryClient."); - // Worker should empty it's queue until SentryOptions.ShutdownTimeout - Worker.FlushAsync(_options.ShutdownTimeout).GetAwaiter().GetResult(); + await Metrics.DisposeAsync().ConfigureAwait(false); - // TODO: Implement async... should probably move Metrics to the Hub and do it in Hub.Dispose as well - if (Metrics is IDisposable disposableMetrics) - { - _options.LogDebug("Flushing MetricsAggregator"); - disposableMetrics.Dispose(); - } + // Worker should empty it's queue until SentryOptions.ShutdownTimeout + await Worker.FlushAsync(_options.ShutdownTimeout).ConfigureAwait(false); } + + /// + /// Disposes this client + /// + /// + public void Dispose() => DisposeAsync().GetAwaiter().GetResult(); } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt index 70505c4a3a..2a8103e0ef 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt @@ -242,9 +242,10 @@ namespace Sentry { void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger); } - public interface IMetricAggregator + public interface IMetricAggregator : System.IAsyncDisposable { void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + System.Threading.Tasks.Task FlushAsync(bool force = true, System.Threading.CancellationToken cancellationToken = default); void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); @@ -485,7 +486,7 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SdkVersion FromJson(System.Text.Json.JsonElement json) { } } - public class SentryClient : Sentry.ISentryClient, System.IDisposable + public class SentryClient : Sentry.ISentryClient, System.IAsyncDisposable, System.IDisposable { public SentryClient(Sentry.SentryOptions options) { } public bool IsEnabled { get; } @@ -496,6 +497,7 @@ namespace Sentry public void CaptureTransaction(Sentry.Transaction transaction, Sentry.Scope? scope, Sentry.Hint? hint) { } public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void Dispose() { } + public System.Threading.Tasks.ValueTask DisposeAsync() { } public System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } } public static class SentryClientExtensions diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt index 70505c4a3a..2a8103e0ef 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt @@ -242,9 +242,10 @@ namespace Sentry { void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger); } - public interface IMetricAggregator + public interface IMetricAggregator : System.IAsyncDisposable { void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + System.Threading.Tasks.Task FlushAsync(bool force = true, System.Threading.CancellationToken cancellationToken = default); void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); @@ -485,7 +486,7 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SdkVersion FromJson(System.Text.Json.JsonElement json) { } } - public class SentryClient : Sentry.ISentryClient, System.IDisposable + public class SentryClient : Sentry.ISentryClient, System.IAsyncDisposable, System.IDisposable { public SentryClient(Sentry.SentryOptions options) { } public bool IsEnabled { get; } @@ -496,6 +497,7 @@ namespace Sentry public void CaptureTransaction(Sentry.Transaction transaction, Sentry.Scope? scope, Sentry.Hint? hint) { } public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void Dispose() { } + public System.Threading.Tasks.ValueTask DisposeAsync() { } public System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } } public static class SentryClientExtensions diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index bfbf1ff3bb..8aff11f681 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -243,9 +243,10 @@ namespace Sentry { void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger); } - public interface IMetricAggregator + public interface IMetricAggregator : System.IAsyncDisposable { void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + System.Threading.Tasks.Task FlushAsync(bool force = true, System.Threading.CancellationToken cancellationToken = default); void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); @@ -486,7 +487,7 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SdkVersion FromJson(System.Text.Json.JsonElement json) { } } - public class SentryClient : Sentry.ISentryClient, System.IDisposable + public class SentryClient : Sentry.ISentryClient, System.IAsyncDisposable, System.IDisposable { public SentryClient(Sentry.SentryOptions options) { } public bool IsEnabled { get; } @@ -497,6 +498,7 @@ namespace Sentry public void CaptureTransaction(Sentry.Transaction transaction, Sentry.Scope? scope, Sentry.Hint? hint) { } public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void Dispose() { } + public System.Threading.Tasks.ValueTask DisposeAsync() { } public System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } } public static class SentryClientExtensions diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 43c652d829..5c0f71ffc0 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -241,9 +241,10 @@ namespace Sentry { void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger); } - public interface IMetricAggregator + public interface IMetricAggregator : System.IAsyncDisposable { void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + System.Threading.Tasks.Task FlushAsync(bool force = true, System.Threading.CancellationToken cancellationToken = default); void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); @@ -484,7 +485,7 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SdkVersion FromJson(System.Text.Json.JsonElement json) { } } - public class SentryClient : Sentry.ISentryClient, System.IDisposable + public class SentryClient : Sentry.ISentryClient, System.IAsyncDisposable, System.IDisposable { public SentryClient(Sentry.SentryOptions options) { } public bool IsEnabled { get; } @@ -495,6 +496,7 @@ namespace Sentry public void CaptureTransaction(Sentry.Transaction transaction, Sentry.Scope? scope, Sentry.Hint? hint) { } public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void Dispose() { } + public System.Threading.Tasks.ValueTask DisposeAsync() { } public System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } } public static class SentryClientExtensions From aa3682fae5403e9028eb4d5c864937b4e93967d9 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 15 Dec 2023 01:05:42 +1300 Subject: [PATCH 29/52] Fixed serialization for code locations --- .../Sentry.Samples.Console.Metrics/Program.cs | 9 ++++--- src/Sentry/DelegatingMetricAggregator.cs | 12 +++++---- src/Sentry/DisabledMetricAggregator.cs | 10 +++---- src/Sentry/IMetricAggregator.cs | 10 +++---- src/Sentry/MetricAggregator.cs | 27 ++++++++++++++----- src/Sentry/Protocol/Metrics/CodeLocations.cs | 9 ++++++- src/Sentry/Timing.cs | 19 ++++++++----- ...piApprovalTests.Run.DotNet6_0.verified.txt | 10 +++---- ...piApprovalTests.Run.DotNet7_0.verified.txt | 10 +++---- ...piApprovalTests.Run.DotNet8_0.verified.txt | 10 +++---- .../ApiApprovalTests.Run.Net4_8.verified.txt | 10 +++---- 11 files changed, 83 insertions(+), 53 deletions(-) diff --git a/samples/Sentry.Samples.Console.Metrics/Program.cs b/samples/Sentry.Samples.Console.Metrics/Program.cs index f0170ebc7a..71720c7a29 100644 --- a/samples/Sentry.Samples.Console.Metrics/Program.cs +++ b/samples/Sentry.Samples.Console.Metrics/Program.cs @@ -17,6 +17,7 @@ private static void Main() options.Debug = true; options.IsGlobalModeEnabled = true; + options.StackTraceMode = StackTraceMode.Enhanced; // Initialize some (non null) ExperimentalMetricsOptions to enable Sentry Metrics, options.ExperimentalMetrics = new ExperimentalMetricsOptions { @@ -26,9 +27,9 @@ private static void Main() })) { System.Console.WriteLine("Measure, Yeah, Measure"); - PlaySetBingo(30); - CreateRevenueGauge(1000); - MeasureShrimp(1000); + PlaySetBingo(10); + // CreateRevenueGauge(1000); + // MeasureShrimp(1000); System.Console.WriteLine("Measure up"); } } @@ -39,7 +40,7 @@ private static void PlaySetBingo(int attempts) // The Timing class creates a distribution that is designed to measure the amount of time it takes to run code // blocks. By default it will use a unit of Seconds - we're configuring it to use milliseconds here though. - using (new Timing(nameof(PlaySetBingo), MeasurementUnit.Duration.Millisecond)) + using (new Timing("bingo", MeasurementUnit.Duration.Millisecond)) { for (var i = 0; i < attempts; i++) { diff --git a/src/Sentry/DelegatingMetricAggregator.cs b/src/Sentry/DelegatingMetricAggregator.cs index 58904039e3..a4c675f865 100644 --- a/src/Sentry/DelegatingMetricAggregator.cs +++ b/src/Sentry/DelegatingMetricAggregator.cs @@ -2,20 +2,22 @@ namespace Sentry; internal class DelegatingMetricAggregator(IMetricAggregator innerAggregator) : IMetricAggregator { + internal IMetricAggregator InnerAggregator => innerAggregator; + public void Increment(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, int stackLevel = 0) => innerAggregator.Increment(key, value, unit, tags, timestamp, stackLevel + 1); + DateTime? timestamp = null, int stackLevel = 1) => innerAggregator.Increment(key, value, unit, tags, timestamp, stackLevel + 1); public void Gauge(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, int stackLevel = 0) => innerAggregator.Gauge(key, value, unit, tags, timestamp, stackLevel + 1); + DateTime? timestamp = null, int stackLevel = 1) => innerAggregator.Gauge(key, value, unit, tags, timestamp, stackLevel + 1); public void Distribution(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, int stackLevel = 0) => innerAggregator.Distribution(key, value, unit, tags, timestamp, stackLevel + 1); + DateTime? timestamp = null, int stackLevel = 1) => innerAggregator.Distribution(key, value, unit, tags, timestamp, stackLevel + 1); public void Set(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, int stackLevel = 0) => innerAggregator.Set(key, value, unit, tags, timestamp, stackLevel + 1); + DateTime? timestamp = null, int stackLevel = 1) => innerAggregator.Set(key, value, unit, tags, timestamp, stackLevel + 1); public void Timing(string key, double value, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, IDictionary? tags = null, - DateTime? timestamp = null, int stackLevel = 0) => innerAggregator.Timing(key, value, unit, tags, timestamp, stackLevel + 1); + DateTime? timestamp = null, int stackLevel = 1) => innerAggregator.Timing(key, value, unit, tags, timestamp, stackLevel + 1); public Task FlushAsync(bool force = true, CancellationToken cancellationToken = default) => innerAggregator.FlushAsync(force, cancellationToken); diff --git a/src/Sentry/DisabledMetricAggregator.cs b/src/Sentry/DisabledMetricAggregator.cs index b52ebe9603..e4d83d83c8 100644 --- a/src/Sentry/DisabledMetricAggregator.cs +++ b/src/Sentry/DisabledMetricAggregator.cs @@ -3,31 +3,31 @@ namespace Sentry; internal class DisabledMetricAggregator : IMetricAggregator { public void Increment(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, int stackLevel = 0) + DateTime? timestamp = null, int stackLevel = 1) { // No Op } public void Gauge(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, int stackLevel = 0) + DateTime? timestamp = null, int stackLevel = 1) { // No Op } public void Distribution(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, int stackLevel = 0) + DateTime? timestamp = null, int stackLevel = 1) { // No Op } public void Set(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, int stackLevel = 0) + DateTime? timestamp = null, int stackLevel = 1) { // No Op } public void Timing(string key, double value, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, IDictionary? tags = null, - DateTime? timestamp = null, int stackLevel = 0) + DateTime? timestamp = null, int stackLevel = 1) { // No Op } diff --git a/src/Sentry/IMetricAggregator.cs b/src/Sentry/IMetricAggregator.cs index 597eb7870a..60ed266b01 100644 --- a/src/Sentry/IMetricAggregator.cs +++ b/src/Sentry/IMetricAggregator.cs @@ -23,7 +23,7 @@ void Increment( MeasurementUnit? unit = null, IDictionary? tags = null, DateTime? timestamp = null, - int stackLevel = 0 + int stackLevel = 1 ); /// @@ -43,7 +43,7 @@ void Gauge( MeasurementUnit? unit = null, IDictionary? tags = null, DateTime? timestamp = null, - int stackLevel = 0 + int stackLevel = 1 ); /// @@ -63,7 +63,7 @@ void Distribution( MeasurementUnit? unit = null, IDictionary? tags = null, DateTime? timestamp = null, - int stackLevel = 0 + int stackLevel = 1 ); /// @@ -83,7 +83,7 @@ void Set( MeasurementUnit? unit = null, IDictionary? tags = null, DateTime? timestamp = null, - int stackLevel = 0 + int stackLevel = 1 ); /// @@ -103,7 +103,7 @@ void Timing( MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, IDictionary? tags = null, DateTime? timestamp = null, - int stackLevel = 0 + int stackLevel = 1 ); /// diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index c93db6c140..7a8b523754 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -23,6 +23,7 @@ internal class MetricAggregator : IMetricAggregator private readonly Lazy>> _buckets = new(() => new ConcurrentDictionary>()); + private long lastClearedStaleLocations = DateTime.UtcNow.GetDayBucketKey(); private readonly HashSet<(long, MetricResourceIdentifier)> _seenLocations = new(); private Dictionary> _pendingLocations = new(); @@ -78,7 +79,7 @@ public void Increment( MeasurementUnit? unit = null, IDictionary? tags = null, DateTime? timestamp = null, - int stackLevel = 0 + int stackLevel = 1 ) => Emit(MetricType.Counter, key, value, unit, tags, timestamp, stackLevel + 1); /// @@ -88,7 +89,7 @@ public void Gauge( MeasurementUnit? unit = null, IDictionary? tags = null, DateTime? timestamp = null, - int stackLevel = 0 + int stackLevel = 1 ) => Emit(MetricType.Gauge, key, value, unit, tags, timestamp, stackLevel + 1); /// @@ -98,7 +99,7 @@ public void Distribution( MeasurementUnit? unit = null, IDictionary? tags = null, DateTime? timestamp = null, - int stackLevel = 0 + int stackLevel = 1 ) => Emit(MetricType.Distribution, key, value, unit, tags, timestamp, stackLevel + 1); /// @@ -108,7 +109,7 @@ public void Set( MeasurementUnit? unit = null, IDictionary? tags = null, DateTime? timestamp = null, - int stackLevel = 0 + int stackLevel = 1 ) => Emit(MetricType.Set, key, value, unit, tags, timestamp, stackLevel + 1); /// @@ -118,7 +119,7 @@ public void Timing( MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, IDictionary? tags = null, DateTime? timestamp = null, - int stackLevel = 0 + int stackLevel = 1 ) => Emit(MetricType.Distribution, key, value, unit, tags, timestamp, stackLevel + 1); private readonly object _emitLock = new object(); @@ -130,7 +131,7 @@ private void Emit( MeasurementUnit? unit = null, IDictionary? tags = null, DateTime? timestamp = null, - int stackLevel = 0 + int stackLevel = 1 ) { timestamp ??= DateTime.UtcNow; @@ -170,7 +171,7 @@ private void Emit( private readonly ReaderWriterLockSlim _codeLocationLock = new(); - private void RecordCodeLocation( + internal void RecordCodeLocation( MetricType type, string key, MeasurementUnit unit, @@ -364,6 +365,18 @@ private Dictionary> _codeLocationLock.EnterWriteLock(); try { + // TODO: Clear out stale seen locations once a day + // var today = DateTime.UtcNow.GetDayBucketKey(); + // if (lastClearedStaleLocations != today) + // { + // var startOfDay = var startOfDay = timestamp.GetDayBucketKey(); + // foreach (var VARIABLE in _seenLocations) + // { + // + // } + // lastClearedStaleLocations = today; + // } + var result = _pendingLocations; _pendingLocations = new Dictionary>(); return result; diff --git a/src/Sentry/Protocol/Metrics/CodeLocations.cs b/src/Sentry/Protocol/Metrics/CodeLocations.cs index 944fb88afd..4fa6488ae9 100644 --- a/src/Sentry/Protocol/Metrics/CodeLocations.cs +++ b/src/Sentry/Protocol/Metrics/CodeLocations.cs @@ -27,8 +27,15 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) loc.IsCodeLocation = true; return loc; }); - writer.WriteDictionary("mapping", mapping, logger); + writer.WritePropertyName("mapping"); + writer.WriteStartObject(); + foreach (var (mri, loc) in mapping) + { + // TODO: Seemingly we can have multiple locations per metric... review how this works with seen locations + writer.WriteArray(mri, new[]{loc}, logger); + } + writer.WriteEndObject(); writer.WriteEndObject(); } } diff --git a/src/Sentry/Timing.cs b/src/Sentry/Timing.cs index d023452c50..d1b1862981 100644 --- a/src/Sentry/Timing.cs +++ b/src/Sentry/Timing.cs @@ -1,4 +1,5 @@ using Sentry.Extensibility; +using Sentry.Protocol.Metrics; namespace Sentry; @@ -20,6 +21,7 @@ public class Timing: IDisposable private readonly IDictionary? _tags; private readonly Stopwatch _stopwatch = new(); private readonly ISpan _span; + private readonly DateTime _startTime = DateTime.UtcNow; /// /// Creates a new instance. @@ -55,11 +57,16 @@ public Timing(IHub hub, string key, MeasurementUnit.Duration unit = MeasurementU } } - // TODO: Record the code location - // # report code locations here for better accuracy - // aggregator = _get_aggregator() - // if aggregator is not None: - // aggregator.record_code_location("d", self.key, self.unit, self.stacklevel) + // Report code locations here for better accuracy + var aggregator = hub.Metrics; + while (aggregator is DelegatingMetricAggregator metricsWrapper) + { + aggregator = metricsWrapper.InnerAggregator; + } + if (aggregator is MetricAggregator metrics) + { + metrics.RecordCodeLocation(MetricType.Distribution, key, unit, 2, _startTime); + } } /// @@ -81,7 +88,7 @@ public void Dispose() MeasurementUnit.Duration.Nanosecond => _stopwatch.Elapsed.TotalMilliseconds * 1000000, _ => throw new ArgumentOutOfRangeException(nameof(_unit), _unit, null) }; - _hub.Metrics.Timing(_key, value, _unit, _tags); + _hub.Metrics.Timing(_key, value, _unit, _tags, _startTime); } catch(Exception e) { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt index 2a8103e0ef..f7a630c8ce 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt @@ -244,12 +244,12 @@ namespace Sentry } public interface IMetricAggregator : System.IAsyncDisposable { - void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); System.Threading.Tasks.Task FlushAsync(bool force = true, System.Threading.CancellationToken cancellationToken = default); - void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); - void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); - void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); - void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); + void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); + void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); + void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); } public interface IScopeObserver { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt index 2a8103e0ef..f7a630c8ce 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt @@ -244,12 +244,12 @@ namespace Sentry } public interface IMetricAggregator : System.IAsyncDisposable { - void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); System.Threading.Tasks.Task FlushAsync(bool force = true, System.Threading.CancellationToken cancellationToken = default); - void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); - void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); - void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); - void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); + void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); + void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); + void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); } public interface IScopeObserver { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 8aff11f681..1197aa7311 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -245,12 +245,12 @@ namespace Sentry } public interface IMetricAggregator : System.IAsyncDisposable { - void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); System.Threading.Tasks.Task FlushAsync(bool force = true, System.Threading.CancellationToken cancellationToken = default); - void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); - void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); - void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); - void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); + void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); + void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); + void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); } public interface IScopeObserver { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 5c0f71ffc0..6243ac2c07 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -243,12 +243,12 @@ namespace Sentry } public interface IMetricAggregator : System.IAsyncDisposable { - void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); System.Threading.Tasks.Task FlushAsync(bool force = true, System.Threading.CancellationToken cancellationToken = default); - void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); - void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); - void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); - void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 0); + void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); + void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); + void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); + void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); } public interface IScopeObserver { From d9918d8106e7e7c9655daab9feb59f6427c90efb Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 15 Dec 2023 12:13:47 +1300 Subject: [PATCH 30/52] Update Timing.cs --- src/Sentry/Timing.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sentry/Timing.cs b/src/Sentry/Timing.cs index d1b1862981..0ea70722ed 100644 --- a/src/Sentry/Timing.cs +++ b/src/Sentry/Timing.cs @@ -65,7 +65,7 @@ public Timing(IHub hub, string key, MeasurementUnit.Duration unit = MeasurementU } if (aggregator is MetricAggregator metrics) { - metrics.RecordCodeLocation(MetricType.Distribution, key, unit, 2, _startTime); + metrics.RecordCodeLocation(MetricType.Distribution, key, unit, 1, _startTime); } } From 6de5909dd405ffd42a936d99e4620d99eba2400f Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 15 Dec 2023 13:59:21 +1300 Subject: [PATCH 31/52] Clear stale seen periods at the end of each day --- .../Sentry.Samples.Console.Metrics.csproj | 5 ++ src/Sentry/MetricAggregator.cs | 83 +++++++++++-------- 2 files changed, 52 insertions(+), 36 deletions(-) diff --git a/samples/Sentry.Samples.Console.Metrics/Sentry.Samples.Console.Metrics.csproj b/samples/Sentry.Samples.Console.Metrics/Sentry.Samples.Console.Metrics.csproj index beb1ec147b..f8f9c4078d 100644 --- a/samples/Sentry.Samples.Console.Metrics/Sentry.Samples.Console.Metrics.csproj +++ b/samples/Sentry.Samples.Console.Metrics/Sentry.Samples.Console.Metrics.csproj @@ -5,6 +5,11 @@ net8.0 + + full + true + + diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index 7a8b523754..aa511b436e 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -24,7 +24,7 @@ private readonly Lazy new ConcurrentDictionary>()); private long lastClearedStaleLocations = DateTime.UtcNow.GetDayBucketKey(); - private readonly HashSet<(long, MetricResourceIdentifier)> _seenLocations = new(); + private readonly ConcurrentDictionary> _seenLocations = new(); private Dictionary> _pendingLocations = new(); private Task LoopTask { get; } @@ -169,7 +169,7 @@ private void Emit( } } - private readonly ReaderWriterLockSlim _codeLocationLock = new(); + private readonly SemaphoreSlim _codeLocationLock = new(1,1); internal void RecordCodeLocation( MetricType type, @@ -182,37 +182,30 @@ DateTime timestamp var startOfDay = timestamp.GetDayBucketKey(); var metaKey = new MetricResourceIdentifier(type, key, unit); - _codeLocationLock.EnterUpgradeableReadLock(); + var seenToday = _seenLocations.GetOrAdd(startOfDay,_ => []); + if (seenToday.Contains(metaKey)) + { + return; + } + _codeLocationLock.Wait(); try { - if (_seenLocations.Contains((startOfDay, metaKey))) + // Group metadata by day to make flushing more efficient. + seenToday.Add(metaKey); + if (GetCodeLocation(stackLevel + 1) is not { } location) { return; } - _codeLocationLock.EnterWriteLock(); - try - { - // Group metadata by day to make flushing more efficient. - _seenLocations.Add((startOfDay, metaKey)); - if (GetCodeLocation(stackLevel + 1) is not { } location) - { - return; - } - if (!_pendingLocations.ContainsKey(startOfDay)) - { - _pendingLocations[startOfDay] = new Dictionary(); - } - _pendingLocations[startOfDay][metaKey] = location; - } - finally + if (!_pendingLocations.ContainsKey(startOfDay)) { - _codeLocationLock.ExitWriteLock(); + _pendingLocations[startOfDay] = new Dictionary(); } + _pendingLocations[startOfDay][metaKey] = location; } finally { - _codeLocationLock.ExitUpgradeableReadLock(); + _codeLocationLock.Release(); } } @@ -309,6 +302,8 @@ public async Task FlushAsync(bool force = true, CancellationToken cancellationTo _captureCodeLocations(codeLocations); _options.LogDebug("Code locations flushed: ", timestamp); } + + ClearStaleLocations(); } catch (OperationCanceledException) { @@ -362,29 +357,45 @@ internal IEnumerable GetFlushableBuckets(bool force = false) private Dictionary> FlushableLocations() { - _codeLocationLock.EnterWriteLock(); + _codeLocationLock.Wait(); try { - // TODO: Clear out stale seen locations once a day - // var today = DateTime.UtcNow.GetDayBucketKey(); - // if (lastClearedStaleLocations != today) - // { - // var startOfDay = var startOfDay = timestamp.GetDayBucketKey(); - // foreach (var VARIABLE in _seenLocations) - // { - // - // } - // lastClearedStaleLocations = today; - // } - var result = _pendingLocations; _pendingLocations = new Dictionary>(); return result; } finally { - _codeLocationLock.ExitWriteLock(); + _codeLocationLock.Release(); + } + } + + /// + /// Clear out stale seen locations once a day + /// + private void ClearStaleLocations() + { + var now = DateTime.UtcNow; + var today = now.GetDayBucketKey(); + if (lastClearedStaleLocations == today) + { + return; + } + // Allow 60 seconds for all code locations to be sent at the transition from one day to the next + const int staleGraceInMinutes = 1; + if (now.Minute < staleGraceInMinutes) + { + return; + } + + foreach (var dailyValues in _seenLocations.Keys.ToArray()) + { + if (dailyValues < today) + { + _seenLocations.TryRemove(dailyValues, out _); + } } + lastClearedStaleLocations = today; } /// From d180a9d07cb6bf9cc966f6b6705b73d8d5a5815f Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 15 Dec 2023 14:35:27 +1300 Subject: [PATCH 32/52] Update CodeLocations.cs --- src/Sentry/Protocol/Metrics/CodeLocations.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Sentry/Protocol/Metrics/CodeLocations.cs b/src/Sentry/Protocol/Metrics/CodeLocations.cs index 4fa6488ae9..807e8daf3c 100644 --- a/src/Sentry/Protocol/Metrics/CodeLocations.cs +++ b/src/Sentry/Protocol/Metrics/CodeLocations.cs @@ -32,7 +32,9 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) writer.WriteStartObject(); foreach (var (mri, loc) in mapping) { - // TODO: Seemingly we can have multiple locations per metric... review how this works with seen locations + // The protocol supports multiple locations per MRI but currently the Sentry Relay will discard all but the + // first, so even though we only capture a single location we send it through as an array. + // See: https://discord.com/channels/621778831602221064/1184350202774163556/1185010167369170974 writer.WriteArray(mri, new[]{loc}, logger); } writer.WriteEndObject(); From 2a6f344c979ab857d2fca6193452d660a0b13409 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 15 Dec 2023 15:45:11 +1300 Subject: [PATCH 33/52] Fixed stacklevel when calling one of the two Timing constructors --- src/Sentry/Timing.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Sentry/Timing.cs b/src/Sentry/Timing.cs index 0ea70722ed..2f216339df 100644 --- a/src/Sentry/Timing.cs +++ b/src/Sentry/Timing.cs @@ -28,7 +28,7 @@ public class Timing: IDisposable /// public Timing(string key, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, IDictionary? tags = null) - : this(SentrySdk.CurrentHub, key, unit, tags) + : this(SentrySdk.CurrentHub, key, unit, tags, stackLevel: 1) { } @@ -37,6 +37,12 @@ public Timing(string key, MeasurementUnit.Duration unit = MeasurementUnit.Durati /// public Timing(IHub hub, string key, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, IDictionary? tags = null) + : this(hub, key, unit, tags, stackLevel: 1) + { + } + + internal Timing(IHub hub, string key, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, + IDictionary? tags = null, int stackLevel = 1) { _hub = hub; _key = key; @@ -65,7 +71,7 @@ public Timing(IHub hub, string key, MeasurementUnit.Duration unit = MeasurementU } if (aggregator is MetricAggregator metrics) { - metrics.RecordCodeLocation(MetricType.Distribution, key, unit, 1, _startTime); + metrics.RecordCodeLocation(MetricType.Distribution, key, unit, stackLevel + 1, _startTime); } } From 052c7a98b1d1e359b580002c7751f1e9c74e8f77 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 18 Dec 2023 15:11:08 +1300 Subject: [PATCH 34/52] Removed IAsyncDisposable from MetricAggregator --- src/Sentry/DelegatingMetricAggregator.cs | 2 +- src/Sentry/DisabledMetricAggregator.cs | 3 +-- src/Sentry/IMetricAggregator.cs | 2 +- src/Sentry/MetricAggregator.cs | 7 ++++++- src/Sentry/SentryClient.cs | 18 +++++++----------- ...ApiApprovalTests.Run.DotNet6_0.verified.txt | 5 ++--- ...ApiApprovalTests.Run.DotNet7_0.verified.txt | 5 ++--- ...ApiApprovalTests.Run.DotNet8_0.verified.txt | 5 ++--- .../ApiApprovalTests.Run.Net4_8.verified.txt | 5 ++--- 9 files changed, 24 insertions(+), 28 deletions(-) diff --git a/src/Sentry/DelegatingMetricAggregator.cs b/src/Sentry/DelegatingMetricAggregator.cs index a4c675f865..410a912cab 100644 --- a/src/Sentry/DelegatingMetricAggregator.cs +++ b/src/Sentry/DelegatingMetricAggregator.cs @@ -22,5 +22,5 @@ public void Timing(string key, double value, MeasurementUnit.Duration unit = Mea public Task FlushAsync(bool force = true, CancellationToken cancellationToken = default) => innerAggregator.FlushAsync(force, cancellationToken); - public ValueTask DisposeAsync() => innerAggregator.DisposeAsync(); + public void Dispose() => innerAggregator.Dispose(); } diff --git a/src/Sentry/DisabledMetricAggregator.cs b/src/Sentry/DisabledMetricAggregator.cs index e4d83d83c8..40516d0fd5 100644 --- a/src/Sentry/DisabledMetricAggregator.cs +++ b/src/Sentry/DisabledMetricAggregator.cs @@ -38,9 +38,8 @@ public Task FlushAsync(bool force = true, CancellationToken cancellationToken = return Task.CompletedTask; } - public ValueTask DisposeAsync() + public void Dispose() { // No Op - return default; } } diff --git a/src/Sentry/IMetricAggregator.cs b/src/Sentry/IMetricAggregator.cs index 60ed266b01..82c27969bb 100644 --- a/src/Sentry/IMetricAggregator.cs +++ b/src/Sentry/IMetricAggregator.cs @@ -4,7 +4,7 @@ namespace Sentry; /// Exposes EXPERIMENTAL capability to emit metrics. This API is subject to change without major version bumps so use /// with caution. We advise disabling in production at the moment. /// -public interface IMetricAggregator: IAsyncDisposable +public interface IMetricAggregator: IDisposable { /// /// Emits a Counter metric diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index aa511b436e..32b5020986 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -398,7 +398,7 @@ private void ClearStaleLocations() lastClearedStaleLocations = today; } - /// + /// public async ValueTask DisposeAsync() { _options.LogDebug("Disposing MetricAggregator."); @@ -437,4 +437,9 @@ public async ValueTask DisposeAsync() LoopTask.Dispose(); } } + + public void Dispose() + { + DisposeAsync().GetAwaiter().GetResult(); + } } diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index fde60527fe..2473c6b82c 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -14,7 +14,7 @@ namespace Sentry; /// /// /// -public class SentryClient : ISentryClient, IDisposable, IAsyncDisposable +public class SentryClient : ISentryClient, IDisposable { private readonly SentryOptions _options; private readonly ISessionManager _sessionManager; @@ -462,20 +462,16 @@ private bool CaptureEnvelope(Envelope envelope) return @event; } - /// - public async ValueTask DisposeAsync() + /// + /// Disposes this client + /// + public void Dispose() { _options.LogDebug("Flushing SentryClient."); - await Metrics.DisposeAsync().ConfigureAwait(false); + Metrics.FlushAsync().GetAwaiter().GetResult(); // Worker should empty it's queue until SentryOptions.ShutdownTimeout - await Worker.FlushAsync(_options.ShutdownTimeout).ConfigureAwait(false); + Worker.FlushAsync(_options.ShutdownTimeout).GetAwaiter().GetResult(); } - - /// - /// Disposes this client - /// - /// - public void Dispose() => DisposeAsync().GetAwaiter().GetResult(); } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt index f7a630c8ce..864b6dc441 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt @@ -242,7 +242,7 @@ namespace Sentry { void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger); } - public interface IMetricAggregator : System.IAsyncDisposable + public interface IMetricAggregator : System.IDisposable { void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); System.Threading.Tasks.Task FlushAsync(bool force = true, System.Threading.CancellationToken cancellationToken = default); @@ -486,7 +486,7 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SdkVersion FromJson(System.Text.Json.JsonElement json) { } } - public class SentryClient : Sentry.ISentryClient, System.IAsyncDisposable, System.IDisposable + public class SentryClient : Sentry.ISentryClient, System.IDisposable { public SentryClient(Sentry.SentryOptions options) { } public bool IsEnabled { get; } @@ -497,7 +497,6 @@ namespace Sentry public void CaptureTransaction(Sentry.Transaction transaction, Sentry.Scope? scope, Sentry.Hint? hint) { } public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void Dispose() { } - public System.Threading.Tasks.ValueTask DisposeAsync() { } public System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } } public static class SentryClientExtensions diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt index f7a630c8ce..864b6dc441 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt @@ -242,7 +242,7 @@ namespace Sentry { void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger); } - public interface IMetricAggregator : System.IAsyncDisposable + public interface IMetricAggregator : System.IDisposable { void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); System.Threading.Tasks.Task FlushAsync(bool force = true, System.Threading.CancellationToken cancellationToken = default); @@ -486,7 +486,7 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SdkVersion FromJson(System.Text.Json.JsonElement json) { } } - public class SentryClient : Sentry.ISentryClient, System.IAsyncDisposable, System.IDisposable + public class SentryClient : Sentry.ISentryClient, System.IDisposable { public SentryClient(Sentry.SentryOptions options) { } public bool IsEnabled { get; } @@ -497,7 +497,6 @@ namespace Sentry public void CaptureTransaction(Sentry.Transaction transaction, Sentry.Scope? scope, Sentry.Hint? hint) { } public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void Dispose() { } - public System.Threading.Tasks.ValueTask DisposeAsync() { } public System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } } public static class SentryClientExtensions diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 1197aa7311..c44d181619 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -243,7 +243,7 @@ namespace Sentry { void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger); } - public interface IMetricAggregator : System.IAsyncDisposable + public interface IMetricAggregator : System.IDisposable { void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); System.Threading.Tasks.Task FlushAsync(bool force = true, System.Threading.CancellationToken cancellationToken = default); @@ -487,7 +487,7 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SdkVersion FromJson(System.Text.Json.JsonElement json) { } } - public class SentryClient : Sentry.ISentryClient, System.IAsyncDisposable, System.IDisposable + public class SentryClient : Sentry.ISentryClient, System.IDisposable { public SentryClient(Sentry.SentryOptions options) { } public bool IsEnabled { get; } @@ -498,7 +498,6 @@ namespace Sentry public void CaptureTransaction(Sentry.Transaction transaction, Sentry.Scope? scope, Sentry.Hint? hint) { } public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void Dispose() { } - public System.Threading.Tasks.ValueTask DisposeAsync() { } public System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } } public static class SentryClientExtensions diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 6243ac2c07..ff1b6f252d 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -241,7 +241,7 @@ namespace Sentry { void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger); } - public interface IMetricAggregator : System.IAsyncDisposable + public interface IMetricAggregator : System.IDisposable { void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); System.Threading.Tasks.Task FlushAsync(bool force = true, System.Threading.CancellationToken cancellationToken = default); @@ -485,7 +485,7 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SdkVersion FromJson(System.Text.Json.JsonElement json) { } } - public class SentryClient : Sentry.ISentryClient, System.IAsyncDisposable, System.IDisposable + public class SentryClient : Sentry.ISentryClient, System.IDisposable { public SentryClient(Sentry.SentryOptions options) { } public bool IsEnabled { get; } @@ -496,7 +496,6 @@ namespace Sentry public void CaptureTransaction(Sentry.Transaction transaction, Sentry.Scope? scope, Sentry.Hint? hint) { } public void CaptureUserFeedback(Sentry.UserFeedback userFeedback) { } public void Dispose() { } - public System.Threading.Tasks.ValueTask DisposeAsync() { } public System.Threading.Tasks.Task FlushAsync(System.TimeSpan timeout) { } } public static class SentryClientExtensions From 2eedd9cf4c494636de101d03ca8ac71b9d5e468d Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 18 Dec 2023 17:25:11 +1300 Subject: [PATCH 35/52] Cherry picked https://github.com/getsentry/Ben.Demystifier/pull/4 --- modules/Ben.Demystifier | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/Ben.Demystifier b/modules/Ben.Demystifier index dfdee44890..137ef343ca 160000 --- a/modules/Ben.Demystifier +++ b/modules/Ben.Demystifier @@ -1 +1 @@ -Subproject commit dfdee448905e7685e56c6231768ea70ac2b20052 +Subproject commit 137ef343ca773edfcf765dd27128cc6204d5b7a1 From c72c7e196532d20f2b422a869cb97ad48154ecdb Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 18 Dec 2023 21:58:25 +1300 Subject: [PATCH 36/52] Update Ben.Demystifier --- modules/Ben.Demystifier | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/Ben.Demystifier b/modules/Ben.Demystifier index 137ef343ca..2e7a3fd9d6 160000 --- a/modules/Ben.Demystifier +++ b/modules/Ben.Demystifier @@ -1 +1 @@ -Subproject commit 137ef343ca773edfcf765dd27128cc6204d5b7a1 +Subproject commit 2e7a3fd9d6e848326a20377cc9de602cb57c7af6 From 2a323c522436d81fdfd976549c8d3d6c49140284 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 19 Dec 2023 16:15:40 +1300 Subject: [PATCH 37/52] Get line numbers with stack traces without enhanced stack traces --- src/Sentry/MetricAggregator.cs | 2 +- src/Sentry/Timing.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index 32b5020986..8c703319b6 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -211,7 +211,7 @@ DateTime timestamp internal SentryStackFrame? GetCodeLocation(int stackLevel) { - var stackTrace = new StackTrace(false); + var stackTrace = new StackTrace(true); var frames = DebugStackTrace.Create(_options, stackTrace, false).Frames; return (frames.Count >= stackLevel) ? frames[^(stackLevel + 1)] diff --git a/src/Sentry/Timing.cs b/src/Sentry/Timing.cs index 2f216339df..5c3858031f 100644 --- a/src/Sentry/Timing.cs +++ b/src/Sentry/Timing.cs @@ -28,7 +28,7 @@ public class Timing: IDisposable /// public Timing(string key, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, IDictionary? tags = null) - : this(SentrySdk.CurrentHub, key, unit, tags, stackLevel: 1) + : this(SentrySdk.CurrentHub, key, unit, tags, stackLevel: 2 /* one for each constructor */) { } @@ -37,12 +37,12 @@ public Timing(string key, MeasurementUnit.Duration unit = MeasurementUnit.Durati /// public Timing(IHub hub, string key, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, IDictionary? tags = null) - : this(hub, key, unit, tags, stackLevel: 1) + : this(hub, key, unit, tags, stackLevel: 2 /* one for each constructor */) { } - internal Timing(IHub hub, string key, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, - IDictionary? tags = null, int stackLevel = 1) + internal Timing(IHub hub, string key, MeasurementUnit.Duration unit, IDictionary? tags, + int stackLevel) { _hub = hub; _key = key; From 57e2b12cc645933c7da3bd30e196dc9f0dc40736 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 19 Dec 2023 16:18:59 +1300 Subject: [PATCH 38/52] Update Ben.Demystifier --- modules/Ben.Demystifier | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/Ben.Demystifier b/modules/Ben.Demystifier index 2e7a3fd9d6..27c0910993 160000 --- a/modules/Ben.Demystifier +++ b/modules/Ben.Demystifier @@ -1 +1 @@ -Subproject commit 2e7a3fd9d6e848326a20377cc9de602cb57c7af6 +Subproject commit 27c091099317f50d80b16ce306a56698d48a8430 From d2c6cb51360088493f68454425d8f988b9e2fc8a Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 19 Dec 2023 16:20:00 +1300 Subject: [PATCH 39/52] Reversed changes to AspNetCore.Basic sample (unrelated to this PR) --- samples/Sentry.Samples.AspNetCore.Basic/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/Sentry.Samples.AspNetCore.Basic/Program.cs b/samples/Sentry.Samples.AspNetCore.Basic/Program.cs index b55e432a51..d982123b56 100644 --- a/samples/Sentry.Samples.AspNetCore.Basic/Program.cs +++ b/samples/Sentry.Samples.AspNetCore.Basic/Program.cs @@ -15,7 +15,7 @@ public static IWebHost BuildWebHost(string[] args) => .UseSentry(o => { // A DSN is required. You can set it here, or in configuration, or in an environment variable. - o.Dsn = "https://b887218a80114d26a9b1a51c5f88e0b4@o447951.ingest.sentry.io/6601807"; + o.Dsn = "https://eb18e953812b41c3aeb042e666fd3b5c@o447951.ingest.sentry.io/5428537"; // Enable Sentry performance monitoring o.EnableTracing = true; From a9c5d6b25f7b2577239234374c38a43adad0b07b Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 19 Dec 2023 16:32:59 +1300 Subject: [PATCH 40/52] Update Program.cs --- .../Sentry.Samples.Console.Metrics/Program.cs | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/samples/Sentry.Samples.Console.Metrics/Program.cs b/samples/Sentry.Samples.Console.Metrics/Program.cs index 71720c7a29..f3f694103f 100644 --- a/samples/Sentry.Samples.Console.Metrics/Program.cs +++ b/samples/Sentry.Samples.Console.Metrics/Program.cs @@ -26,10 +26,35 @@ private static void Main() }; })) { - System.Console.WriteLine("Measure, Yeah, Measure"); - PlaySetBingo(10); - // CreateRevenueGauge(1000); - // MeasureShrimp(1000); + System.Console.WriteLine("Measure, Yeah, Measure!"); + while (true) + { + // Perform your task here + switch (Roll.Next(1,3)) + { + case 1: + PlaySetBingo(10); + break; + case 2: + CreateRevenueGauge(100); + break; + case 3: + MeasureShrimp(30); + break; + } + + + // Optional: Delay to prevent tight looping + var sleepTime = Roll.Next(1, 10); + System.Console.WriteLine($"Sleeping for {sleepTime} second(s)."); + System.Console.WriteLine("Press any key to stop..."); + Thread.Sleep(TimeSpan.FromSeconds(sleepTime)); + // Check if a key has been pressed + if (System.Console.KeyAvailable) + { + break; + } + } System.Console.WriteLine("Measure up"); } } From 30ef3b4f66626d37c87c670b2348c7fdb63bf9a5 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 19 Dec 2023 16:52:41 +1300 Subject: [PATCH 41/52] Improved the lock when incrementing/adding to existing metrics --- src/Sentry/MetricAggregator.cs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index 8c703319b6..b331700e1d 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -151,17 +151,22 @@ private void Emit( _ => new ConcurrentDictionary() ); - lock (_emitLock) - { - timeBucket.AddOrUpdate( - GetMetricBucketKey(type, key, unit.Value, tags), - addValuesFactory, - (_, metric) => + timeBucket.AddOrUpdate( + GetMetricBucketKey(type, key, unit.Value, tags), + addValuesFactory, + (_, metric) => + { + // This prevents multiple threads from trying to mutate the metric at the same time. The only other + // operations performed against metrics are adding one to the bucket (guaranteed to be atomic due to + // the use of a ConcurrentDictionary for the timeBucket) and removing buckets entirely. Technically, + // with a very small flushShift (e.g. 0.0) it might be possible to get a metric emitted to a bucket that + // is being removed after a flush... + lock(metric) { metric.Add(value); - return metric; - }); - } + } + return metric; + }); if (_options.ExperimentalMetrics is { EnableCodeLocations: true }) { From 34a5c3eb793134a48a7929fde81984215cc16b00 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 19 Dec 2023 17:31:40 +1300 Subject: [PATCH 42/52] Tweaking docs --- src/Sentry/MetricAggregator.cs | 11 ++++++++--- src/Sentry/SentryOptions.cs | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index b331700e1d..48e4538aa7 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -158,9 +158,14 @@ private void Emit( { // This prevents multiple threads from trying to mutate the metric at the same time. The only other // operations performed against metrics are adding one to the bucket (guaranteed to be atomic due to - // the use of a ConcurrentDictionary for the timeBucket) and removing buckets entirely. Technically, - // with a very small flushShift (e.g. 0.0) it might be possible to get a metric emitted to a bucket that - // is being removed after a flush... + // the use of a ConcurrentDictionary for the timeBucket) and removing buckets entirely. + // + // With a very small flushShift (e.g. 0.0) it might be possible for a metric to be emitted to a bucket + // that was removed after a flush, in which case that metric.Add(value) would never make it to Sentry. + // We've never seen this happen in unit testing (where we always set the flushShift to 0.0) so this + // remains only a theoretical possibility of data loss (not confirmed). If this becomes a real problem + // and we need to guarantee delivery of every metric.Add, we'll need to build a more complex mechanism + // to coordinate flushing with emission. lock(metric) { metric.Add(value); diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index da7ded203e..caa9916933 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -1316,7 +1316,7 @@ internal enum DefaultIntegrations } /// -/// Settings to the experimental Metrics feature. This feature is preview only and will very likely change in the future +/// Settings for the experimental Metrics feature. This feature is preview only and will very likely change in the future /// without a major version bump... so use at your own risk. /// public class ExperimentalMetricsOptions From 7faa57cd5daef6b5d73bcde7ba1666ac5bc6534a Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 21 Dec 2023 10:03:26 +1300 Subject: [PATCH 43/52] Update CHANGELOG.md Co-authored-by: Bruno Garcia --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6911010c90..9376766f54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features -- Experimental pre-release availability of Delightful Developer Metrics. We're exploring the use of Metrics in Sentry. The API will very likely change and we don't yet have any documentation. ([#2949](https://github.com/getsentry/sentry-dotnet/pull/2949)) +- Experimental pre-release availability of Metrics. We're exploring the use of Metrics in Sentry. The API will very likely change and we don't yet have any documentation. ([#2949](https://github.com/getsentry/sentry-dotnet/pull/2949)) ### Dependencies From d956b5d65532ff26afff11a11f59082a2d22f185 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 21 Dec 2023 10:56:16 +1300 Subject: [PATCH 44/52] Integrating review feedback --- samples/Sentry.Samples.Console.Metrics/Program.cs | 1 - .../Sentry.Samples.Console.Metrics.csproj | 5 ----- src/Sentry/MetricAggregator.cs | 15 +++++++++++++-- src/Sentry/MetricHelper.cs | 4 ++-- src/Sentry/Protocol/Metrics/DistributionMetric.cs | 14 ++++++++------ src/Sentry/Protocol/Metrics/GaugeMetric.cs | 2 +- src/Sentry/Protocol/Metrics/SetMetric.cs | 14 ++++++++------ 7 files changed, 32 insertions(+), 23 deletions(-) diff --git a/samples/Sentry.Samples.Console.Metrics/Program.cs b/samples/Sentry.Samples.Console.Metrics/Program.cs index f3f694103f..ddd7a11ad6 100644 --- a/samples/Sentry.Samples.Console.Metrics/Program.cs +++ b/samples/Sentry.Samples.Console.Metrics/Program.cs @@ -16,7 +16,6 @@ private static void Main() "https://eb18e953812b41c3aeb042e666fd3b5c@o447951.ingest.sentry.io/5428537"; options.Debug = true; - options.IsGlobalModeEnabled = true; options.StackTraceMode = StackTraceMode.Enhanced; // Initialize some (non null) ExperimentalMetricsOptions to enable Sentry Metrics, options.ExperimentalMetrics = new ExperimentalMetricsOptions diff --git a/samples/Sentry.Samples.Console.Metrics/Sentry.Samples.Console.Metrics.csproj b/samples/Sentry.Samples.Console.Metrics/Sentry.Samples.Console.Metrics.csproj index f8f9c4078d..beb1ec147b 100644 --- a/samples/Sentry.Samples.Console.Metrics/Sentry.Samples.Console.Metrics.csproj +++ b/samples/Sentry.Samples.Console.Metrics/Sentry.Samples.Console.Metrics.csproj @@ -5,11 +5,6 @@ net8.0 - - full - true - - diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index 48e4538aa7..c84f110d35 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -438,7 +438,7 @@ public async ValueTask DisposeAsync() } catch (Exception exception) { - _options.LogError(exception, "Stopping the Metric Aggregator threw an exception."); + _options.LogError(exception, "Async Disposing the Metric Aggregator threw an exception."); } finally { @@ -450,6 +450,17 @@ public async ValueTask DisposeAsync() public void Dispose() { - DisposeAsync().GetAwaiter().GetResult(); + try + { + DisposeAsync().AsTask().GetAwaiter().GetResult(); + } + catch (OperationCanceledException) + { + // Ignore + } + catch (Exception exception) + { + _options.LogError(exception, "Disposing the Metric Aggregator threw an exception."); + } } } diff --git a/src/Sentry/MetricHelper.cs b/src/Sentry/MetricHelper.cs index 744de6b832..e968e838ac 100644 --- a/src/Sentry/MetricHelper.cs +++ b/src/Sentry/MetricHelper.cs @@ -35,6 +35,6 @@ internal static DateTime GetCutoff() => DateTime.UtcNow .Subtract(TimeSpan.FromSeconds(RollupInSeconds)) .Subtract(TimeSpan.FromMilliseconds(FlushShift)); - internal static string SanitizeKey(string input) => Regex.Replace(input, @"[^a-zA-Z0-9_/.-]+", "_"); - internal static string SanitizeValue(string input) => Regex.Replace(input, @"[^\w\d_:/@\.\{\}\[\]$-]+", "_"); + internal static string SanitizeKey(string input) => Regex.Replace(input, @"[^a-zA-Z0-9_/.-]+", "_", RegexOptions.Compiled); + internal static string SanitizeValue(string input) => Regex.Replace(input, @"[^\w\d_:/@\.\{\}\[\]$-]+", "_", RegexOptions.Compiled); } diff --git a/src/Sentry/Protocol/Metrics/DistributionMetric.cs b/src/Sentry/Protocol/Metrics/DistributionMetric.cs index 42235157f1..faf5967028 100644 --- a/src/Sentry/Protocol/Metrics/DistributionMetric.cs +++ b/src/Sentry/Protocol/Metrics/DistributionMetric.cs @@ -10,26 +10,28 @@ internal class DistributionMetric : Metric { public DistributionMetric() { - Value = new List(); + _value = new List(); } public DistributionMetric(string key, double value, MeasurementUnit? unit = null, IDictionary? tags = null, DateTime? timestamp = null) : base(key, unit, tags, timestamp) { - Value = new List() { value }; + _value = new List() { value }; } - public IList Value { get; set; } + private readonly List _value; + + public IReadOnlyList Value => _value; public override void Add(double value) { - Value.Add(value); + _value.Add(value); } protected override void WriteValues(Utf8JsonWriter writer, IDiagnosticLogger? logger) => - writer.WriteArrayIfNotEmpty("value", Value, logger); + writer.WriteArrayIfNotEmpty("value", _value, logger); protected override IEnumerable SerializedStatsdValues() - => Value.Select(v => (IConvertible)v); + => _value.Cast(); } diff --git a/src/Sentry/Protocol/Metrics/GaugeMetric.cs b/src/Sentry/Protocol/Metrics/GaugeMetric.cs index 1bd48d345b..994a1dc093 100644 --- a/src/Sentry/Protocol/Metrics/GaugeMetric.cs +++ b/src/Sentry/Protocol/Metrics/GaugeMetric.cs @@ -30,7 +30,7 @@ public GaugeMetric(string key, double value, MeasurementUnit? unit = null, IDict } public double Value { get; private set; } - public double First { get; private set; } + public double First { get; } public double Min { get; private set; } public double Max { get; private set; } public double Sum { get; private set; } diff --git a/src/Sentry/Protocol/Metrics/SetMetric.cs b/src/Sentry/Protocol/Metrics/SetMetric.cs index d26ed6716f..ce21c0434d 100644 --- a/src/Sentry/Protocol/Metrics/SetMetric.cs +++ b/src/Sentry/Protocol/Metrics/SetMetric.cs @@ -10,26 +10,28 @@ internal class SetMetric : Metric { public SetMetric() { - Value = new HashSet(); + _value = new HashSet(); } public SetMetric(string key, int value, MeasurementUnit? unit = null, IDictionary? tags = null, DateTime? timestamp = null) : base(key, unit, tags, timestamp) { - Value = new HashSet() { value }; + _value = new HashSet() { value }; } - public HashSet Value { get; private set; } + public IReadOnlyCollection Value => _value; + + private readonly HashSet _value; public override void Add(double value) { - Value.Add((int)value); + _value.Add((int)value); } protected override void WriteValues(Utf8JsonWriter writer, IDiagnosticLogger? logger) => - writer.WriteArrayIfNotEmpty("value", Value, logger); + writer.WriteArrayIfNotEmpty("value", _value, logger); protected override IEnumerable SerializedStatsdValues() - => Value.Select(v => (IConvertible)v); + => _value.Select(v => (IConvertible)v); } From 3efab8a4ced28e173cd14b1c11957df661bd2584 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 21 Dec 2023 11:09:37 +1300 Subject: [PATCH 45/52] Source generated RegEx in metric helper --- src/Sentry/MetricHelper.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Sentry/MetricHelper.cs b/src/Sentry/MetricHelper.cs index e968e838ac..8dbd6910aa 100644 --- a/src/Sentry/MetricHelper.cs +++ b/src/Sentry/MetricHelper.cs @@ -1,6 +1,6 @@ namespace Sentry; -internal static class MetricHelper +internal static partial class MetricHelper { private const int RollupInSeconds = 10; @@ -35,6 +35,16 @@ internal static DateTime GetCutoff() => DateTime.UtcNow .Subtract(TimeSpan.FromSeconds(RollupInSeconds)) .Subtract(TimeSpan.FromMilliseconds(FlushShift)); +#if NET7_0_OR_GREATER + [GeneratedRegex(@"[^a-zA-Z0-9_/.-]+", RegexOptions.Compiled)] + private static partial Regex InvalidKeyCharacters(); + internal static string SanitizeKey(string input) => InvalidKeyCharacters().Replace(input, "_"); + + [GeneratedRegex(@"[^\w\d_:/@\.\{\}\[\]$-]+", RegexOptions.Compiled)] + private static partial Regex InvalidValueCharacters(); + internal static string SanitizeValue(string input) => InvalidValueCharacters().Replace(input, "_"); +#else internal static string SanitizeKey(string input) => Regex.Replace(input, @"[^a-zA-Z0-9_/.-]+", "_", RegexOptions.Compiled); internal static string SanitizeValue(string input) => Regex.Replace(input, @"[^\w\d_:/@\.\{\}\[\]$-]+", "_", RegexOptions.Compiled); +#endif } From c68c25cb91cb59c003e5e0af8e69a9cfb0d530fc Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 21 Dec 2023 11:49:09 +1300 Subject: [PATCH 46/52] Update Envelope.cs --- src/Sentry/Protocol/Envelopes/Envelope.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Sentry/Protocol/Envelopes/Envelope.cs b/src/Sentry/Protocol/Envelopes/Envelope.cs index 3b936873ba..5d48fb0cd6 100644 --- a/src/Sentry/Protocol/Envelopes/Envelope.cs +++ b/src/Sentry/Protocol/Envelopes/Envelope.cs @@ -333,8 +333,7 @@ internal static Envelope FromCodeLocations(CodeLocations codeLocations) { var header = DefaultHeader; - List items = new(); - items.Add(EnvelopeItem.FromCodeLocations(codeLocations)); + List items = [ EnvelopeItem.FromCodeLocations(codeLocations) ]; return new Envelope(header, items); } From 4cb9ef746a0cf78b565f66186d4753938de53469 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 27 Dec 2023 21:47:36 +1300 Subject: [PATCH 47/52] Review feedback --- src/Sentry/DelegatingMetricAggregator.cs | 25 ++++---- src/Sentry/DisabledMetricAggregator.cs | 25 ++++---- src/Sentry/IMetricAggregator.cs | 50 +++++++--------- src/Sentry/MetricAggregator.cs | 59 ++++++++----------- src/Sentry/MetricHelper.cs | 18 +++--- src/Sentry/Protocol/Envelopes/Envelope.cs | 3 +- src/Sentry/Protocol/Metrics/CounterMetric.cs | 2 +- .../Protocol/Metrics/DistributionMetric.cs | 11 ++-- src/Sentry/Protocol/Metrics/GaugeMetric.cs | 2 +- src/Sentry/Protocol/Metrics/Metric.cs | 23 +++++--- src/Sentry/Protocol/Metrics/SetMetric.cs | 11 ++-- src/Sentry/Timing.cs | 2 +- ...piApprovalTests.Run.DotNet6_0.verified.txt | 10 ++-- ...piApprovalTests.Run.DotNet7_0.verified.txt | 10 ++-- ...piApprovalTests.Run.DotNet8_0.verified.txt | 10 ++-- .../ApiApprovalTests.Run.Net4_8.verified.txt | 10 ++-- test/Sentry.Tests/MetricAggregatorTests.cs | 29 ++++----- test/Sentry.Tests/MetricBucketHelperTests.cs | 5 +- 18 files changed, 152 insertions(+), 153 deletions(-) diff --git a/src/Sentry/DelegatingMetricAggregator.cs b/src/Sentry/DelegatingMetricAggregator.cs index 410a912cab..49f0cb5ece 100644 --- a/src/Sentry/DelegatingMetricAggregator.cs +++ b/src/Sentry/DelegatingMetricAggregator.cs @@ -4,20 +4,25 @@ internal class DelegatingMetricAggregator(IMetricAggregator innerAggregator) : I { internal IMetricAggregator InnerAggregator => innerAggregator; - public void Increment(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, int stackLevel = 1) => innerAggregator.Increment(key, value, unit, tags, timestamp, stackLevel + 1); + public void Increment(string key, double value = 1.0, MeasurementUnit? unit = null, + IDictionary? tags = null, + DateTimeOffset? timestamp = null, int stackLevel = 1) => innerAggregator.Increment(key, value, unit, tags, timestamp, stackLevel + 1); - public void Gauge(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, int stackLevel = 1) => innerAggregator.Gauge(key, value, unit, tags, timestamp, stackLevel + 1); + public void Gauge(string key, double value = 1.0, MeasurementUnit? unit = null, + IDictionary? tags = null, + DateTimeOffset? timestamp = null, int stackLevel = 1) => innerAggregator.Gauge(key, value, unit, tags, timestamp, stackLevel + 1); - public void Distribution(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, int stackLevel = 1) => innerAggregator.Distribution(key, value, unit, tags, timestamp, stackLevel + 1); + public void Distribution(string key, double value = 1.0, MeasurementUnit? unit = null, + IDictionary? tags = null, + DateTimeOffset? timestamp = null, int stackLevel = 1) => innerAggregator.Distribution(key, value, unit, tags, timestamp, stackLevel + 1); - public void Set(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, int stackLevel = 1) => innerAggregator.Set(key, value, unit, tags, timestamp, stackLevel + 1); + public void Set(string key, double value = 1.0, MeasurementUnit? unit = null, + IDictionary? tags = null, + DateTimeOffset? timestamp = null, int stackLevel = 1) => innerAggregator.Set(key, value, unit, tags, timestamp, stackLevel + 1); - public void Timing(string key, double value, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, IDictionary? tags = null, - DateTime? timestamp = null, int stackLevel = 1) => innerAggregator.Timing(key, value, unit, tags, timestamp, stackLevel + 1); + public void Timing(string key, double value, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, + IDictionary? tags = null, + DateTimeOffset? timestamp = null, int stackLevel = 1) => innerAggregator.Timing(key, value, unit, tags, timestamp, stackLevel + 1); public Task FlushAsync(bool force = true, CancellationToken cancellationToken = default) => innerAggregator.FlushAsync(force, cancellationToken); diff --git a/src/Sentry/DisabledMetricAggregator.cs b/src/Sentry/DisabledMetricAggregator.cs index 40516d0fd5..ed42ea1b6b 100644 --- a/src/Sentry/DisabledMetricAggregator.cs +++ b/src/Sentry/DisabledMetricAggregator.cs @@ -2,32 +2,37 @@ namespace Sentry; internal class DisabledMetricAggregator : IMetricAggregator { - public void Increment(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, int stackLevel = 1) + public void Increment(string key, double value = 1.0, MeasurementUnit? unit = null, + IDictionary? tags = null, + DateTimeOffset? timestamp = null, int stackLevel = 1) { // No Op } - public void Gauge(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, int stackLevel = 1) + public void Gauge(string key, double value = 1.0, MeasurementUnit? unit = null, + IDictionary? tags = null, + DateTimeOffset? timestamp = null, int stackLevel = 1) { // No Op } - public void Distribution(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, int stackLevel = 1) + public void Distribution(string key, double value = 1.0, MeasurementUnit? unit = null, + IDictionary? tags = null, + DateTimeOffset? timestamp = null, int stackLevel = 1) { // No Op } - public void Set(string key, double value = 1, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, int stackLevel = 1) + public void Set(string key, double value = 1.0, MeasurementUnit? unit = null, + IDictionary? tags = null, + DateTimeOffset? timestamp = null, int stackLevel = 1) { // No Op } - public void Timing(string key, double value, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, IDictionary? tags = null, - DateTime? timestamp = null, int stackLevel = 1) + public void Timing(string key, double value, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, + IDictionary? tags = null, + DateTimeOffset? timestamp = null, int stackLevel = 1) { // No Op } diff --git a/src/Sentry/IMetricAggregator.cs b/src/Sentry/IMetricAggregator.cs index 82c27969bb..162897e708 100644 --- a/src/Sentry/IMetricAggregator.cs +++ b/src/Sentry/IMetricAggregator.cs @@ -14,17 +14,15 @@ public interface IMetricAggregator: IDisposable /// An optional /// Optional Tags to associate with the metric /// - /// The time when the metric was emitted. Defaults to the time at which the metric is emitted, if no value is provided. + /// The time when the metric was emitted. Defaults to the time at which the metric is emitted, if no value is provided. /// /// Optional number of stacks levels to ignore when determining the code location - void Increment( - string key, + void Increment(string key, double value = 1.0, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, - int stackLevel = 1 - ); + DateTimeOffset? timestamp = null, + int stackLevel = 1); /// /// Emits a Gauge metric @@ -34,17 +32,15 @@ void Increment( /// An optional /// Optional Tags to associate with the metric /// - /// The time when the metric was emitted. Defaults to the time at which the metric is emitted, if no value is provided. + /// The time when the metric was emitted. Defaults to the time at which the metric is emitted, if no value is provided. /// /// Optional number of stacks levels to ignore when determining the code location - void Gauge( - string key, + void Gauge(string key, double value = 1.0, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, - int stackLevel = 1 - ); + DateTimeOffset? timestamp = null, + int stackLevel = 1); /// /// Emits a Distribution metric @@ -54,17 +50,15 @@ void Gauge( /// An optional /// Optional Tags to associate with the metric /// - /// The time when the metric was emitted. Defaults to the time at which the metric is emitted, if no value is provided. + /// The time when the metric was emitted. Defaults to the time at which the metric is emitted, if no value is provided. /// /// Optional number of stacks levels to ignore when determining the code location - void Distribution( - string key, + void Distribution(string key, double value = 1.0, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, - int stackLevel = 1 - ); + DateTimeOffset? timestamp = null, + int stackLevel = 1); /// /// Emits a Set metric @@ -74,17 +68,15 @@ void Distribution( /// An optional /// Optional Tags to associate with the metric /// - /// The time when the metric was emitted. Defaults to the time at which the metric is emitted, if no value is provided. + /// The time when the metric was emitted. Defaults to the time at which the metric is emitted, if no value is provided. /// /// Optional number of stacks levels to ignore when determining the code location - void Set( - string key, + void Set(string key, double value = 1.0, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, - int stackLevel = 1 - ); + DateTimeOffset? timestamp = null, + int stackLevel = 1); /// /// Emits a distribution with the time it takes to run a given code block. @@ -92,19 +84,17 @@ void Set( /// A unique key identifying the metric /// The value to be added /// - /// An optional . Defaults to + /// An optional . Defaults to /// /// Optional Tags to associate with the metric /// The time when the metric was emitted /// Optional number of stacks levels to ignore when determining the code location - void Timing( - string key, + void Timing(string key, double value, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, IDictionary? tags = null, - DateTime? timestamp = null, - int stackLevel = 1 - ); + DateTimeOffset? timestamp = null, + int stackLevel = 1); /// /// Flushes any flushable metrics and/or code locations. diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index c84f110d35..565ea8dfbf 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -1,3 +1,4 @@ +using System; using Sentry.Extensibility; using Sentry.Internal; using Sentry.Internal.Extensions; @@ -23,7 +24,7 @@ internal class MetricAggregator : IMetricAggregator private readonly Lazy>> _buckets = new(() => new ConcurrentDictionary>()); - private long lastClearedStaleLocations = DateTime.UtcNow.GetDayBucketKey(); + private long lastClearedStaleLocations = DateTimeOffset.UtcNow.GetDayBucketKey(); private readonly ConcurrentDictionary> _seenLocations = new(); private Dictionary> _pendingLocations = new(); @@ -73,54 +74,44 @@ internal static string GetMetricBucketKey(MetricType type, string metricKey, Mea } /// - public void Increment( - string key, + public void Increment(string key, double value = 1.0, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, - int stackLevel = 1 - ) => Emit(MetricType.Counter, key, value, unit, tags, timestamp, stackLevel + 1); + DateTimeOffset? timestamp = null, + int stackLevel = 1) => Emit(MetricType.Counter, key, value, unit, tags, timestamp, stackLevel + 1); /// - public void Gauge( - string key, + public void Gauge(string key, double value = 1.0, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, - int stackLevel = 1 - ) => Emit(MetricType.Gauge, key, value, unit, tags, timestamp, stackLevel + 1); + DateTimeOffset? timestamp = null, + int stackLevel = 1) => Emit(MetricType.Gauge, key, value, unit, tags, timestamp, stackLevel + 1); /// - public void Distribution( - string key, + public void Distribution(string key, double value = 1.0, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, - int stackLevel = 1 - ) => Emit(MetricType.Distribution, key, value, unit, tags, timestamp, stackLevel + 1); + DateTimeOffset? timestamp = null, + int stackLevel = 1) => Emit(MetricType.Distribution, key, value, unit, tags, timestamp, stackLevel + 1); /// - public void Set( - string key, + public void Set(string key, double value = 1.0, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, - int stackLevel = 1 - ) => Emit(MetricType.Set, key, value, unit, tags, timestamp, stackLevel + 1); + DateTimeOffset? timestamp = null, + int stackLevel = 1) => Emit(MetricType.Set, key, value, unit, tags, timestamp, stackLevel + 1); /// - public void Timing( - string key, + public void Timing(string key, double value, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, IDictionary? tags = null, - DateTime? timestamp = null, - int stackLevel = 1 - ) => Emit(MetricType.Distribution, key, value, unit, tags, timestamp, stackLevel + 1); + DateTimeOffset? timestamp = null, + int stackLevel = 1) => Emit(MetricType.Distribution, key, value, unit, tags, timestamp, stackLevel + 1); private readonly object _emitLock = new object(); @@ -130,11 +121,11 @@ private void Emit( double value = 1.0, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null, + DateTimeOffset? timestamp = null, int stackLevel = 1 ) { - timestamp ??= DateTime.UtcNow; + timestamp ??= DateTimeOffset.UtcNow; unit ??= MeasurementUnit.None; Func addValuesFactory = type switch @@ -186,7 +177,7 @@ internal void RecordCodeLocation( string key, MeasurementUnit unit, int stackLevel, - DateTime timestamp + DateTimeOffset timestamp ) { var startOfDay = timestamp.GetDayBucketKey(); @@ -207,11 +198,13 @@ DateTime timestamp return; } - if (!_pendingLocations.ContainsKey(startOfDay)) + if (!_pendingLocations.TryGetValue(startOfDay, out var todaysLocations)) { - _pendingLocations[startOfDay] = new Dictionary(); + todaysLocations = new Dictionary(); + _pendingLocations[startOfDay] = todaysLocations; } - _pendingLocations[startOfDay][metaKey] = location; + + todaysLocations[metaKey] = location; } finally { @@ -385,7 +378,7 @@ private Dictionary> /// private void ClearStaleLocations() { - var now = DateTime.UtcNow; + var now = DateTimeOffset.UtcNow; var today = now.GetDayBucketKey(); if (lastClearedStaleLocations == today) { diff --git a/src/Sentry/MetricHelper.cs b/src/Sentry/MetricHelper.cs index 8dbd6910aa..3a59e1f274 100644 --- a/src/Sentry/MetricHelper.cs +++ b/src/Sentry/MetricHelper.cs @@ -5,19 +5,19 @@ internal static partial class MetricHelper private const int RollupInSeconds = 10; #if NET6_0_OR_GREATER - static readonly DateTime UnixEpoch = DateTime.UnixEpoch; + private static readonly DateTimeOffset UnixEpoch = DateTimeOffset.UnixEpoch; #else - static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + private static readonly DateTimeOffset UnixEpoch = new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero); #endif - internal static long GetDayBucketKey(this DateTime timestamp) + internal static long GetDayBucketKey(this DateTimeOffset timestamp) { var utc = timestamp.ToUniversalTime(); - var dayOnly = new DateTime(utc.Year, utc.Month, utc.Day, 0, 0, 0, 0, DateTimeKind.Utc); + var dayOnly = new DateTimeOffset(utc.Year, utc.Month, utc.Day, 0, 0, 0, 0, TimeSpan.Zero); return (long)(dayOnly - UnixEpoch).TotalSeconds; } - internal static long GetTimeBucketKey(this DateTime timestamp) + internal static long GetTimeBucketKey(this DateTimeOffset timestamp) { var seconds = (long)(timestamp.ToUniversalTime() - UnixEpoch).TotalSeconds; return (seconds / RollupInSeconds) * RollupInSeconds; @@ -31,7 +31,7 @@ internal static long GetTimeBucketKey(this DateTime timestamp) /// /// Internal for testing internal static double FlushShift = new Random().Next(0, 1000) * RollupInSeconds; - internal static DateTime GetCutoff() => DateTime.UtcNow + internal static DateTimeOffset GetCutoff() => DateTimeOffset.UtcNow .Subtract(TimeSpan.FromSeconds(RollupInSeconds)) .Subtract(TimeSpan.FromMilliseconds(FlushShift)); @@ -44,7 +44,9 @@ internal static DateTime GetCutoff() => DateTime.UtcNow private static partial Regex InvalidValueCharacters(); internal static string SanitizeValue(string input) => InvalidValueCharacters().Replace(input, "_"); #else - internal static string SanitizeKey(string input) => Regex.Replace(input, @"[^a-zA-Z0-9_/.-]+", "_", RegexOptions.Compiled); - internal static string SanitizeValue(string input) => Regex.Replace(input, @"[^\w\d_:/@\.\{\}\[\]$-]+", "_", RegexOptions.Compiled); + private static readonly Regex InvalidKeyCharacters = new(@"[^a-zA-Z0-9_/.-]+", RegexOptions.Compiled); + internal static string SanitizeKey(string input) => InvalidKeyCharacters.Replace(input, "_"); + private static readonly Regex InvalidValueCharacters = new(@"[^\w\d_:/@\.\{\}\[\]$-]+", RegexOptions.Compiled); + internal static string SanitizeValue(string input) => InvalidValueCharacters.Replace(input, "_"); #endif } diff --git a/src/Sentry/Protocol/Envelopes/Envelope.cs b/src/Sentry/Protocol/Envelopes/Envelope.cs index 5d48fb0cd6..681dbf19d2 100644 --- a/src/Sentry/Protocol/Envelopes/Envelope.cs +++ b/src/Sentry/Protocol/Envelopes/Envelope.cs @@ -333,7 +333,8 @@ internal static Envelope FromCodeLocations(CodeLocations codeLocations) { var header = DefaultHeader; - List items = [ EnvelopeItem.FromCodeLocations(codeLocations) ]; + var items = new List(1); + items.Add(EnvelopeItem.FromCodeLocations(codeLocations)); return new Envelope(header, items); } diff --git a/src/Sentry/Protocol/Metrics/CounterMetric.cs b/src/Sentry/Protocol/Metrics/CounterMetric.cs index ffaffe4fad..81778fc640 100644 --- a/src/Sentry/Protocol/Metrics/CounterMetric.cs +++ b/src/Sentry/Protocol/Metrics/CounterMetric.cs @@ -13,7 +13,7 @@ public CounterMetric() } public CounterMetric(string key, double value, MeasurementUnit? unit = null, IDictionary? tags = null, DateTime? timestamp = null) + string>? tags = null, DateTimeOffset? timestamp = null) : base(key, unit, tags, timestamp) { Value = value; diff --git a/src/Sentry/Protocol/Metrics/DistributionMetric.cs b/src/Sentry/Protocol/Metrics/DistributionMetric.cs index faf5967028..7d6a14ffe3 100644 --- a/src/Sentry/Protocol/Metrics/DistributionMetric.cs +++ b/src/Sentry/Protocol/Metrics/DistributionMetric.cs @@ -8,26 +8,23 @@ namespace Sentry.Protocol.Metrics; /// internal class DistributionMetric : Metric { + private readonly List _value; + public DistributionMetric() { _value = new List(); } public DistributionMetric(string key, double value, MeasurementUnit? unit = null, - IDictionary? tags = null, DateTime? timestamp = null) + IDictionary? tags = null, DateTimeOffset? timestamp = null) : base(key, unit, tags, timestamp) { _value = new List() { value }; } - private readonly List _value; - public IReadOnlyList Value => _value; - public override void Add(double value) - { - _value.Add(value); - } + public override void Add(double value) => _value.Add(value); protected override void WriteValues(Utf8JsonWriter writer, IDiagnosticLogger? logger) => writer.WriteArrayIfNotEmpty("value", _value, logger); diff --git a/src/Sentry/Protocol/Metrics/GaugeMetric.cs b/src/Sentry/Protocol/Metrics/GaugeMetric.cs index 994a1dc093..cefd262038 100644 --- a/src/Sentry/Protocol/Metrics/GaugeMetric.cs +++ b/src/Sentry/Protocol/Metrics/GaugeMetric.cs @@ -18,7 +18,7 @@ public GaugeMetric() } public GaugeMetric(string key, double value, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null) + DateTimeOffset? timestamp = null) : base(key, unit, tags, timestamp) { Value = value; diff --git a/src/Sentry/Protocol/Metrics/Metric.cs b/src/Sentry/Protocol/Metrics/Metric.cs index 0cc3d05ef9..34ff3b7060 100644 --- a/src/Sentry/Protocol/Metrics/Metric.cs +++ b/src/Sentry/Protocol/Metrics/Metric.cs @@ -10,23 +10,32 @@ protected Metric() : this(string.Empty) { } - protected Metric(string key, MeasurementUnit? unit = null, IDictionary? tags = null, DateTime? timestamp = null) + protected Metric(string key, MeasurementUnit? unit = null, IDictionary? tags = null, DateTimeOffset? timestamp = null) { Key = key; Unit = unit; - Tags = tags ?? new Dictionary(); - Timestamp = timestamp ?? DateTime.UtcNow; + _tags = tags; + Timestamp = timestamp ?? DateTimeOffset.UtcNow; } public SentryId EventId { get; private set; } = SentryId.Create(); public string Key { get; private set; } - public DateTime Timestamp { get; private set; } + public DateTimeOffset Timestamp { get; private set; } public MeasurementUnit? Unit { get; private set; } - public IDictionary Tags { get; private set; } + private IDictionary? _tags; + + public IDictionary Tags + { + get + { + _tags ??= new Dictionary(); + return _tags; + } + } public abstract void Add(double value); @@ -43,7 +52,7 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) { writer.WriteStringIfNotWhiteSpace("unit", Unit.ToString()); } - writer.WriteStringDictionaryIfNotEmpty("tags", Tags!); + writer.WriteStringDictionaryIfNotEmpty("tags", (IEnumerable>?)_tags); WriteValues(writer, logger); writer.WriteEndObject(); } @@ -69,7 +78,7 @@ public async Task SerializeAsync(Stream stream, IDiagnosticLogger? logger, Cance await Write($"|{StatsdType}"); - if (Tags is { Count: > 0 } tags) + if (_tags is { Count: > 0 } tags) { await Write("|#"); var first = true; diff --git a/src/Sentry/Protocol/Metrics/SetMetric.cs b/src/Sentry/Protocol/Metrics/SetMetric.cs index ce21c0434d..ac102bdf37 100644 --- a/src/Sentry/Protocol/Metrics/SetMetric.cs +++ b/src/Sentry/Protocol/Metrics/SetMetric.cs @@ -8,13 +8,15 @@ namespace Sentry.Protocol.Metrics; /// internal class SetMetric : Metric { + private readonly HashSet _value; + public SetMetric() { _value = new HashSet(); } public SetMetric(string key, int value, MeasurementUnit? unit = null, IDictionary? tags = null, - DateTime? timestamp = null) + DateTimeOffset? timestamp = null) : base(key, unit, tags, timestamp) { _value = new HashSet() { value }; @@ -22,12 +24,7 @@ public SetMetric(string key, int value, MeasurementUnit? unit = null, IDictionar public IReadOnlyCollection Value => _value; - private readonly HashSet _value; - - public override void Add(double value) - { - _value.Add((int)value); - } + public override void Add(double value) => _value.Add((int)value); protected override void WriteValues(Utf8JsonWriter writer, IDiagnosticLogger? logger) => writer.WriteArrayIfNotEmpty("value", _value, logger); diff --git a/src/Sentry/Timing.cs b/src/Sentry/Timing.cs index 5c3858031f..d32b94cd79 100644 --- a/src/Sentry/Timing.cs +++ b/src/Sentry/Timing.cs @@ -96,7 +96,7 @@ public void Dispose() }; _hub.Metrics.Timing(_key, value, _unit, _tags, _startTime); } - catch(Exception e) + catch (Exception e) { _hub.GetSentryOptions()?.LogError(e, "Error capturing timing"); } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt index 864b6dc441..9f79d80159 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt @@ -244,12 +244,12 @@ namespace Sentry } public interface IMetricAggregator : System.IDisposable { - void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); + void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); System.Threading.Tasks.Task FlushAsync(bool force = true, System.Threading.CancellationToken cancellationToken = default); - void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); - void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); - void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); - void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); + void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); + void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); + void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); + void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); } public interface IScopeObserver { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt index 864b6dc441..9f79d80159 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt @@ -244,12 +244,12 @@ namespace Sentry } public interface IMetricAggregator : System.IDisposable { - void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); + void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); System.Threading.Tasks.Task FlushAsync(bool force = true, System.Threading.CancellationToken cancellationToken = default); - void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); - void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); - void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); - void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); + void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); + void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); + void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); + void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); } public interface IScopeObserver { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index c44d181619..b238264eab 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -245,12 +245,12 @@ namespace Sentry } public interface IMetricAggregator : System.IDisposable { - void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); + void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); System.Threading.Tasks.Task FlushAsync(bool force = true, System.Threading.CancellationToken cancellationToken = default); - void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); - void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); - void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); - void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); + void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); + void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); + void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); + void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); } public interface IScopeObserver { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index ff1b6f252d..49d557bf51 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -243,12 +243,12 @@ namespace Sentry } public interface IMetricAggregator : System.IDisposable { - void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); + void Distribution(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); System.Threading.Tasks.Task FlushAsync(bool force = true, System.Threading.CancellationToken cancellationToken = default); - void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); - void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); - void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); - void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTime? timestamp = default, int stackLevel = 1); + void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); + void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); + void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); + void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); } public interface IScopeObserver { diff --git a/test/Sentry.Tests/MetricAggregatorTests.cs b/test/Sentry.Tests/MetricAggregatorTests.cs index ade162deff..3d32fa8794 100644 --- a/test/Sentry.Tests/MetricAggregatorTests.cs +++ b/test/Sentry.Tests/MetricAggregatorTests.cs @@ -45,13 +45,13 @@ public void Increment_AggregatesMetrics() var sut = _fixture.GetSut(); // Act - DateTime firstTime = new(1970, 1, 1, 0, 0, 31, 0, DateTimeKind.Utc); + DateTimeOffset firstTime = new(1970, 1, 1, 0, 0, 31, 0, TimeSpan.Zero); sut.Increment(key, 3, unit, tags, firstTime); - DateTime secondTime = new(1970, 1, 1, 0, 0, 38, 0, DateTimeKind.Utc); + DateTimeOffset secondTime = new(1970, 1, 1, 0, 0, 38, 0, TimeSpan.Zero); sut.Increment(key, 5, unit, tags, secondTime); - DateTime thirdTime = new(1970, 1, 1, 0, 0, 40, 0, DateTimeKind.Utc); + DateTimeOffset thirdTime = new(1970, 1, 1, 0, 0, 40, 0, TimeSpan.Zero); sut.Increment(key, 13, unit, tags, thirdTime); // Assert @@ -75,13 +75,13 @@ public void Gauge_AggregatesMetrics() var sut = _fixture.GetSut(); // Act - DateTime time1 = new(1970, 1, 1, 0, 0, 31, 0, DateTimeKind.Utc); + DateTimeOffset time1 = new(1970, 1, 1, 0, 0, 31, 0, TimeSpan.Zero); sut.Gauge(key, 3, unit, tags, time1); - DateTime time2 = new(1970, 1, 1, 0, 0, 38, 0, DateTimeKind.Utc); + DateTimeOffset time2 = new(1970, 1, 1, 0, 0, 38, 0, TimeSpan.Zero); sut.Gauge(key, 5, unit, tags, time2); - DateTime time3 = new(1970, 1, 1, 0, 0, 40, 0, DateTimeKind.Utc); + DateTimeOffset time3 = new(1970, 1, 1, 0, 0, 40, 0, TimeSpan.Zero); sut.Gauge(key, 13, unit, tags, time3); // Assert @@ -115,13 +115,13 @@ public void Distribution_AggregatesMetrics() var sut = _fixture.GetSut(); // Act - DateTime time1 = new(1970, 1, 1, 0, 0, 31, 0, DateTimeKind.Utc); + DateTimeOffset time1 = new(1970, 1, 1, 0, 0, 31, 0, TimeSpan.Zero); sut.Distribution(key, 3, unit, tags, time1); - DateTime time2 = new(1970, 1, 1, 0, 0, 38, 0, DateTimeKind.Utc); + DateTimeOffset time2 = new(1970, 1, 1, 0, 0, 38, 0, TimeSpan.Zero); sut.Distribution(key, 5, unit, tags, time2); - DateTime time3 = new(1970, 1, 1, 0, 0, 40, 0, DateTimeKind.Utc); + DateTimeOffset time3 = new(1970, 1, 1, 0, 0, 40, 0, TimeSpan.Zero); sut.Distribution(key, 13, unit, tags, time3); // Assert @@ -145,16 +145,16 @@ public void Set_AggregatesMetrics() var sut = _fixture.GetSut(); // Act - DateTime time1 = new(1970, 1, 1, 0, 0, 31, 0, DateTimeKind.Utc); + DateTimeOffset time1 = new(1970, 1, 1, 0, 0, 31, 0, TimeSpan.Zero); sut.Set(key, 3, unit, tags, time1); - DateTime time2 = new(1970, 1, 1, 0, 0, 38, 0, DateTimeKind.Utc); + DateTimeOffset time2 = new(1970, 1, 1, 0, 0, 38, 0, TimeSpan.Zero); sut.Set(key, 5, unit, tags, time2); - DateTime time3 = new(1970, 1, 1, 0, 0, 40, 0, DateTimeKind.Utc); + DateTimeOffset time3 = new(1970, 1, 1, 0, 0, 40, 0, TimeSpan.Zero); sut.Set(key, 13, unit, tags, time3); - DateTime time4 = new(1970, 1, 1, 0, 0, 42, 0, DateTimeKind.Utc); + DateTimeOffset time4 = new(1970, 1, 1, 0, 0, 42, 0, TimeSpan.Zero); sut.Set(key, 13, unit, tags, time3); // Assert @@ -214,7 +214,8 @@ public async Task GetFlushableBuckets_IsThreadsafe() } [Fact] - public void TestGetCodeLocation() { + public void TestGetCodeLocation() + { // Arrange _fixture.Options.StackTraceMode = StackTraceMode.Enhanced; var sut = _fixture.GetSut(); diff --git a/test/Sentry.Tests/MetricBucketHelperTests.cs b/test/Sentry.Tests/MetricBucketHelperTests.cs index 1854987368..5de929afb4 100644 --- a/test/Sentry.Tests/MetricBucketHelperTests.cs +++ b/test/Sentry.Tests/MetricBucketHelperTests.cs @@ -12,8 +12,7 @@ public void GetTimeBucketKey_RoundsDownToNearestTenSeconds(int seconds) { // Arrange // Returns the number of seconds that have elapsed since 1970-01-01T00:00:00Z - // var timestamp = new DateTime(2023, 1, 15, 17, 42, 31, DateTimeKind.Utc); - var timestamp = new DateTime(1970, 1, 1, 1, 1, seconds, DateTimeKind.Utc); + var timestamp = new DateTimeOffset(1970, 1, 1, 1, 1, seconds, TimeSpan.Zero); // Act var result = timestamp.GetTimeBucketKey(); @@ -28,7 +27,7 @@ public void GetTimeBucketKey_RoundsDownToNearestTenSeconds(int seconds) public void GetDayBucketKey_RoundsStartOfDay(int year, int month, int day, int hour, int minute, int second, int expectedDays) { // Arrange - var timestamp = new DateTime(year, month, day, hour, minute, second, DateTimeKind.Utc); + var timestamp = new DateTimeOffset(year, month, day, hour, minute, second, TimeSpan.Zero); // Act var result = timestamp.GetDayBucketKey(); From 8cc439cfee24cd477c8127eb2821b148b9ea7f40 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 4 Jan 2024 11:28:14 +1300 Subject: [PATCH 48/52] Integrating review feedback --- src/Sentry/DelegatingMetricAggregator.cs | 31 -------------------- src/Sentry/Internal/Hub.cs | 2 +- src/Sentry/MetricAggregator.cs | 27 +++++++++-------- src/Sentry/MetricHelper.cs | 6 +++- src/Sentry/Protocol/Metrics/CodeLocations.cs | 9 +++--- src/Sentry/Timing.cs | 7 +---- 6 files changed, 26 insertions(+), 56 deletions(-) delete mode 100644 src/Sentry/DelegatingMetricAggregator.cs diff --git a/src/Sentry/DelegatingMetricAggregator.cs b/src/Sentry/DelegatingMetricAggregator.cs deleted file mode 100644 index 49f0cb5ece..0000000000 --- a/src/Sentry/DelegatingMetricAggregator.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Sentry; - -internal class DelegatingMetricAggregator(IMetricAggregator innerAggregator) : IMetricAggregator -{ - internal IMetricAggregator InnerAggregator => innerAggregator; - - public void Increment(string key, double value = 1.0, MeasurementUnit? unit = null, - IDictionary? tags = null, - DateTimeOffset? timestamp = null, int stackLevel = 1) => innerAggregator.Increment(key, value, unit, tags, timestamp, stackLevel + 1); - - public void Gauge(string key, double value = 1.0, MeasurementUnit? unit = null, - IDictionary? tags = null, - DateTimeOffset? timestamp = null, int stackLevel = 1) => innerAggregator.Gauge(key, value, unit, tags, timestamp, stackLevel + 1); - - public void Distribution(string key, double value = 1.0, MeasurementUnit? unit = null, - IDictionary? tags = null, - DateTimeOffset? timestamp = null, int stackLevel = 1) => innerAggregator.Distribution(key, value, unit, tags, timestamp, stackLevel + 1); - - public void Set(string key, double value = 1.0, MeasurementUnit? unit = null, - IDictionary? tags = null, - DateTimeOffset? timestamp = null, int stackLevel = 1) => innerAggregator.Set(key, value, unit, tags, timestamp, stackLevel + 1); - - public void Timing(string key, double value, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, - IDictionary? tags = null, - DateTimeOffset? timestamp = null, int stackLevel = 1) => innerAggregator.Timing(key, value, unit, tags, timestamp, stackLevel + 1); - - public Task FlushAsync(bool force = true, CancellationToken cancellationToken = default) => - innerAggregator.FlushAsync(force, cancellationToken); - - public void Dispose() => innerAggregator.Dispose(); -} diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index ec4d335329..7f5988321c 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -59,7 +59,7 @@ internal Hub( PushScope(); } - Metrics = new DelegatingMetricAggregator(_ownedClient.Metrics); + Metrics = _ownedClient.Metrics; foreach (var integration in options.Integrations) { diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index 565ea8dfbf..1dc755f3b0 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -13,6 +13,8 @@ internal class MetricAggregator : IMetricAggregator private readonly Action _captureCodeLocations; private readonly TimeSpan _flushInterval; + private readonly SemaphoreSlim _codeLocationLock = new(1,1); + private readonly CancellationTokenSource _shutdownSource; private volatile bool _disposed; @@ -24,7 +26,7 @@ internal class MetricAggregator : IMetricAggregator private readonly Lazy>> _buckets = new(() => new ConcurrentDictionary>()); - private long lastClearedStaleLocations = DateTimeOffset.UtcNow.GetDayBucketKey(); + private long _lastClearedStaleLocations = DateTimeOffset.UtcNow.GetDayBucketKey(); private readonly ConcurrentDictionary> _seenLocations = new(); private Dictionary> _pendingLocations = new(); @@ -170,8 +172,6 @@ private void Emit( } } - private readonly SemaphoreSlim _codeLocationLock = new(1,1); - internal void RecordCodeLocation( MetricType type, string key, @@ -182,17 +182,20 @@ DateTimeOffset timestamp { var startOfDay = timestamp.GetDayBucketKey(); var metaKey = new MetricResourceIdentifier(type, key, unit); - var seenToday = _seenLocations.GetOrAdd(startOfDay,_ => []); - if (seenToday.Contains(metaKey)) - { - return; - } + _codeLocationLock.Wait(); try { // Group metadata by day to make flushing more efficient. - seenToday.Add(metaKey); + if (!seenToday.Add(metaKey)) + { + // If we've seen the location, we don't want to create a stack trace etc. again. It could be a different + // location with the same metaKey but the alternative would be to generate the stack trace every time a + // metric is recorded, which would impact performance too much. + return; + } + if (GetCodeLocation(stackLevel + 1) is not { } location) { return; @@ -380,7 +383,7 @@ private void ClearStaleLocations() { var now = DateTimeOffset.UtcNow; var today = now.GetDayBucketKey(); - if (lastClearedStaleLocations == today) + if (_lastClearedStaleLocations == today) { return; } @@ -398,7 +401,7 @@ private void ClearStaleLocations() _seenLocations.TryRemove(dailyValues, out _); } } - lastClearedStaleLocations = today; + _lastClearedStaleLocations = today; } /// @@ -423,7 +426,7 @@ public async ValueTask DisposeAsync() // NOTE: While non-intuitive, do not pass a timeout or cancellation token here. We are waiting for // the _continuation_ of the method, not its _execution_. If we stop waiting prematurely, this may cause // unexpected behavior in client applications. - LoopTask.Wait(); + await LoopTask.ConfigureAwait(false); } catch (OperationCanceledException) { diff --git a/src/Sentry/MetricHelper.cs b/src/Sentry/MetricHelper.cs index 3a59e1f274..8042bce07f 100644 --- a/src/Sentry/MetricHelper.cs +++ b/src/Sentry/MetricHelper.cs @@ -1,7 +1,11 @@ +using Sentry.Internal; + namespace Sentry; internal static partial class MetricHelper { + private static readonly RandomValuesFactory Random = new SynchronizedRandomValuesFactory(); + private const int RollupInSeconds = 10; #if NET6_0_OR_GREATER @@ -30,7 +34,7 @@ internal static long GetTimeBucketKey(this DateTimeOffset timestamp) /// also apply independent jittering. /// /// Internal for testing - internal static double FlushShift = new Random().Next(0, 1000) * RollupInSeconds; + internal static double FlushShift = Random.NextInt(0, 1000) * RollupInSeconds; internal static DateTimeOffset GetCutoff() => DateTimeOffset.UtcNow .Subtract(TimeSpan.FromSeconds(RollupInSeconds)) .Subtract(TimeSpan.FromMilliseconds(FlushShift)); diff --git a/src/Sentry/Protocol/Metrics/CodeLocations.cs b/src/Sentry/Protocol/Metrics/CodeLocations.cs index 807e8daf3c..fb61b7a13b 100644 --- a/src/Sentry/Protocol/Metrics/CodeLocations.cs +++ b/src/Sentry/Protocol/Metrics/CodeLocations.cs @@ -3,23 +3,22 @@ namespace Sentry.Protocol.Metrics; -internal class CodeLocations(long timestamp, Dictionary locations) + +internal class CodeLocations(long timestamp, IReadOnlyDictionary locations) : IJsonSerializable { /// /// Uniquely identifies a code location using the number of seconds since the UnixEpoch, as measured at the start /// of the day when the code location was recorded. /// - public long Timestamp { get; set; } = timestamp; - - public Dictionary Locations { get; set; } = locations; + public long Timestamp => timestamp; public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) { writer.WriteStartObject(); writer.WriteNumber("timestamp", Timestamp); - var mapping = Locations.ToDictionary( + var mapping = locations.ToDictionary( kvp => kvp.Key.ToString(), kvp => { diff --git a/src/Sentry/Timing.cs b/src/Sentry/Timing.cs index d32b94cd79..21d7a4c3b5 100644 --- a/src/Sentry/Timing.cs +++ b/src/Sentry/Timing.cs @@ -64,12 +64,7 @@ internal Timing(IHub hub, string key, MeasurementUnit.Duration unit, IDictionary } // Report code locations here for better accuracy - var aggregator = hub.Metrics; - while (aggregator is DelegatingMetricAggregator metricsWrapper) - { - aggregator = metricsWrapper.InnerAggregator; - } - if (aggregator is MetricAggregator metrics) + if (hub.Metrics is MetricAggregator metrics) { metrics.RecordCodeLocation(MetricType.Distribution, key, unit, stackLevel + 1, _startTime); } From 705d131195f13b70e60f5dff8b1573ac9313891f Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 4 Jan 2024 13:29:58 +1300 Subject: [PATCH 49/52] More performant string delimited tags used in the bucket key --- src/Sentry/MetricAggregator.cs | 48 +++++++++++++++++++++- test/Sentry.Tests/MetricAggregatorTests.cs | 40 +++++++++++++++++- 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index 1dc755f3b0..0991826f2f 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -70,11 +70,57 @@ internal static string GetMetricBucketKey(MetricType type, string metricKey, Mea IDictionary? tags) { var typePrefix = type.ToStatsdType(); - var serializedTags = tags?.ToUtf8Json() ?? string.Empty; + var serializedTags = GetTagsKey(tags); return $"{typePrefix}_{metricKey}_{unit}_{serializedTags}"; } + internal static string GetTagsKey(IDictionary? tags) + { + if (tags == null || tags.Count == 0) + { + return string.Empty; + } + + const char pairDelimiter = ','; // Delimiter between key-value pairs + const char keyValueDelimiter = '='; // Delimiter between key and value + const char escapeChar = '\\'; + + var builder = new StringBuilder(); + + foreach (var tag in tags) + { + // Escape delimiters in key and value + var key = EscapeString(tag.Key, pairDelimiter, keyValueDelimiter, escapeChar); + var value = EscapeString(tag.Value, pairDelimiter, keyValueDelimiter, escapeChar); + + if (builder.Length > 0) + { + builder.Append(pairDelimiter); + } + + builder.Append(key).Append(keyValueDelimiter).Append(value); + } + + return builder.ToString(); + + static string EscapeString(string input, params char[] charsToEscape) + { + var escapedString = new StringBuilder(input.Length); + + foreach (var ch in input) + { + if (charsToEscape.Contains(ch)) + { + escapedString.Append(escapeChar); // Prefix with escape character + } + escapedString.Append(ch); + } + + return escapedString.ToString(); + } + } + /// public void Increment(string key, double value = 1.0, diff --git a/test/Sentry.Tests/MetricAggregatorTests.cs b/test/Sentry.Tests/MetricAggregatorTests.cs index 3d32fa8794..04d0daeeee 100644 --- a/test/Sentry.Tests/MetricAggregatorTests.cs +++ b/test/Sentry.Tests/MetricAggregatorTests.cs @@ -31,7 +31,7 @@ public void GetMetricBucketKey_GeneratesExpectedKey() var result = MetricAggregator.GetMetricBucketKey(type, metricKey, unit, tags); // Assert - result.Should().Be("c_quibbles_none_{\"tag1\":\"value1\"}"); + result.Should().Be("c_quibbles_none_tag1=value1"); } [Fact] @@ -227,4 +227,42 @@ public void TestGetCodeLocation() result.Should().NotBeNull(); result!.Function.Should().Be($"void {nameof(MetricAggregatorTests)}.{nameof(TestGetCodeLocation)}()"); } + + [Fact] + public void GetTagsKey_ReturnsEmpty_WhenTagsIsNull() + { + var result = MetricAggregator.GetTagsKey(null); + result.Should().BeEmpty(); + } + + [Fact] + public void GetTagsKey_ReturnsEmpty_WhenTagsIsEmpty() + { + var result = MetricAggregator.GetTagsKey(new Dictionary()); + result.Should().BeEmpty(); + } + + [Fact] + public void GetTagsKey_ReturnsValidString_WhenTagsHasOneEntry() + { + var tags = new Dictionary { { "tag1", "value1" } }; + var result = MetricAggregator.GetTagsKey(tags); + result.Should().Be("tag1=value1"); + } + + [Fact] + public void GetTagsKey_ReturnsCorrectString_WhenTagsHasMultipleEntries() + { + var tags = new Dictionary { { "tag1", "value1" }, { "tag2", "value2" } }; + var result = MetricAggregator.GetTagsKey(tags); + result.Should().Be("tag1=value1,tag2=value2"); + } + + [Fact] + public void GetTagsKey_EscapesCharacters_WhenTagsContainDelimiters() + { + var tags = new Dictionary { { "tag1\\", "value1\\" }, { "tag2,", "value2," }, { "tag3=", "value3=" } }; + var result = MetricAggregator.GetTagsKey(tags); + result.Should().Be(@"tag1\\=value1\\,tag2\,=value2\,,tag3\==value3\="); + } } From 353a92887375394f6c5fd0371c48dba969f3955e Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 5 Jan 2024 10:50:19 +1300 Subject: [PATCH 50/52] Integrating review feedback --- src/Sentry/DisabledMetricAggregator.cs | 2 +- src/Sentry/IMetricAggregator.cs | 2 +- src/Sentry/MetricAggregator.cs | 4 ++-- src/Sentry/Protocol/Metrics/CodeLocations.cs | 1 - src/Sentry/SentryClient.cs | 2 +- src/Sentry/Timing.cs | 7 ++----- .../ApiApprovalTests.Run.DotNet6_0.verified.txt | 2 +- .../ApiApprovalTests.Run.DotNet7_0.verified.txt | 2 +- .../ApiApprovalTests.Run.DotNet8_0.verified.txt | 2 +- test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt | 2 +- 10 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/Sentry/DisabledMetricAggregator.cs b/src/Sentry/DisabledMetricAggregator.cs index ed42ea1b6b..61304898cf 100644 --- a/src/Sentry/DisabledMetricAggregator.cs +++ b/src/Sentry/DisabledMetricAggregator.cs @@ -23,7 +23,7 @@ public void Distribution(string key, double value = 1.0, MeasurementUnit? unit = // No Op } - public void Set(string key, double value = 1.0, MeasurementUnit? unit = null, + public void Set(string key, int value, MeasurementUnit? unit = null, IDictionary? tags = null, DateTimeOffset? timestamp = null, int stackLevel = 1) { diff --git a/src/Sentry/IMetricAggregator.cs b/src/Sentry/IMetricAggregator.cs index 162897e708..04bc946b97 100644 --- a/src/Sentry/IMetricAggregator.cs +++ b/src/Sentry/IMetricAggregator.cs @@ -72,7 +72,7 @@ void Distribution(string key, /// /// Optional number of stacks levels to ignore when determining the code location void Set(string key, - double value = 1.0, + int value, MeasurementUnit? unit = null, IDictionary? tags = null, DateTimeOffset? timestamp = null, diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index 0991826f2f..7f02f53339 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -43,7 +43,7 @@ private readonly Lazy /// An optional flushInterval, for testing only - public MetricAggregator(SentryOptions options, Action> captureMetrics, + internal MetricAggregator(SentryOptions options, Action> captureMetrics, Action captureCodeLocations, CancellationTokenSource? shutdownSource = null, bool disableLoopTask = false, TimeSpan? flushInterval = null) { @@ -147,7 +147,7 @@ public void Distribution(string key, /// public void Set(string key, - double value = 1.0, + int value, MeasurementUnit? unit = null, IDictionary? tags = null, DateTimeOffset? timestamp = null, diff --git a/src/Sentry/Protocol/Metrics/CodeLocations.cs b/src/Sentry/Protocol/Metrics/CodeLocations.cs index fb61b7a13b..a8bf10e2f7 100644 --- a/src/Sentry/Protocol/Metrics/CodeLocations.cs +++ b/src/Sentry/Protocol/Metrics/CodeLocations.cs @@ -3,7 +3,6 @@ namespace Sentry.Protocol.Metrics; - internal class CodeLocations(long timestamp, IReadOnlyDictionary locations) : IJsonSerializable { diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index 2473c6b82c..2134d33511 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -246,7 +246,7 @@ public void CaptureTransaction(Transaction transaction, Scope? scope, Hint? hint /// internal void CaptureMetrics(IEnumerable metrics) { - _options.LogDebug($"Capturing metrics"); + _options.LogDebug("Capturing metrics."); CaptureEnvelope(Envelope.FromMetrics(metrics)); } diff --git a/src/Sentry/Timing.cs b/src/Sentry/Timing.cs index 21d7a4c3b5..bdd206d2b1 100644 --- a/src/Sentry/Timing.cs +++ b/src/Sentry/Timing.cs @@ -57,10 +57,7 @@ internal Timing(IHub hub, string key, MeasurementUnit.Duration unit, IDictionary : hub.StartTransaction("metric.timing", key); if (tags is not null) { - foreach (var (k, v) in tags) - { - _span.SetTag(k, v); - } + _span.SetTags(tags); } // Report code locations here for better accuracy @@ -93,7 +90,7 @@ public void Dispose() } catch (Exception e) { - _hub.GetSentryOptions()?.LogError(e, "Error capturing timing"); + _hub.GetSentryOptions()?.LogError(e, "Error capturing timing '{0}'", _key); } finally { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt index cc8984b79a..9b895f7d01 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt @@ -248,7 +248,7 @@ namespace Sentry System.Threading.Tasks.Task FlushAsync(bool force = true, System.Threading.CancellationToken cancellationToken = default); void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); - void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); + void Set(string key, int value, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); } public interface IScopeObserver diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt index cc8984b79a..9b895f7d01 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt @@ -248,7 +248,7 @@ namespace Sentry System.Threading.Tasks.Task FlushAsync(bool force = true, System.Threading.CancellationToken cancellationToken = default); void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); - void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); + void Set(string key, int value, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); } public interface IScopeObserver diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index b00ebe1fe7..b2d08227aa 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -249,7 +249,7 @@ namespace Sentry System.Threading.Tasks.Task FlushAsync(bool force = true, System.Threading.CancellationToken cancellationToken = default); void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); - void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); + void Set(string key, int value, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); } public interface IScopeObserver diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 0481bc6573..c0a2285591 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -247,7 +247,7 @@ namespace Sentry System.Threading.Tasks.Task FlushAsync(bool force = true, System.Threading.CancellationToken cancellationToken = default); void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); - void Set(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); + void Set(string key, int value, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); } public interface IScopeObserver From 589c3b2a039e3553a31bd22f151f4de9396a76c4 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Sun, 7 Jan 2024 12:12:48 +1300 Subject: [PATCH 51/52] Integrated review feedback --- .../Sentry.Samples.Console.Metrics/Program.cs | 7 ++----- src/Sentry/Internal/Hub.cs | 15 ++++++++++----- src/Sentry/MetricAggregator.cs | 10 +++++----- src/Sentry/Protocol/Metrics/Metric.cs | 4 ++-- src/Sentry/SentryClient.cs | 17 ++++++++++++----- 5 files changed, 31 insertions(+), 22 deletions(-) diff --git a/samples/Sentry.Samples.Console.Metrics/Program.cs b/samples/Sentry.Samples.Console.Metrics/Program.cs index ddd7a11ad6..defdb6daf4 100644 --- a/samples/Sentry.Samples.Console.Metrics/Program.cs +++ b/samples/Sentry.Samples.Console.Metrics/Program.cs @@ -72,11 +72,8 @@ private static void PlaySetBingo(int attempts) // This demonstrates the use of a set metric. SentrySdk.Metrics.Gauge("guesses", guess); - if (solution.Contains(guess)) - { - // And this is a counter - SentrySdk.Metrics.Increment("correct_answers"); - } + // And this is a counter + SentrySdk.Metrics.Increment(solution.Contains(guess) ? "correct_answers" : "incorrect_answers"); } } } diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 7f5988321c..9dd0f78250 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -525,11 +525,16 @@ public void Dispose() return; } - var disposeTasks = new List { - _ownedClient.Metrics.FlushAsync(), - _ownedClient.FlushAsync(_options.ShutdownTimeout) - }; - Task.WhenAll(disposeTasks).GetAwaiter().GetResult(); + try + { + _ownedClient.Metrics.FlushAsync().ContinueWith(_ => + _ownedClient.FlushAsync(_options.ShutdownTimeout).Wait() + ).ConfigureAwait(false).GetAwaiter().GetResult(); + } + catch (Exception e) + { + _options.LogError(e, "Failed to wait on disposing tasks to flush."); + } //Dont dispose of ScopeManager since we want dangling transactions to still be able to access tags. #if __IOS__ diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index 7f02f53339..a2f8ac0740 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -30,7 +30,7 @@ private readonly Lazy> _seenLocations = new(); private Dictionary> _pendingLocations = new(); - private Task LoopTask { get; } + private readonly Task _loopTask; /// /// MetricAggregator constructor. @@ -57,12 +57,12 @@ internal MetricAggregator(SentryOptions options, Action> cap { // We can stop the loop from running during testing _options.LogDebug("LoopTask disabled."); - LoopTask = Task.CompletedTask; + _loopTask = Task.CompletedTask; } else { options.LogDebug("Starting MetricsAggregator."); - LoopTask = Task.Run(RunLoopAsync); + _loopTask = Task.Run(RunLoopAsync); } } @@ -472,7 +472,7 @@ public async ValueTask DisposeAsync() // NOTE: While non-intuitive, do not pass a timeout or cancellation token here. We are waiting for // the _continuation_ of the method, not its _execution_. If we stop waiting prematurely, this may cause // unexpected behavior in client applications. - await LoopTask.ConfigureAwait(false); + await _loopTask.ConfigureAwait(false); } catch (OperationCanceledException) { @@ -486,7 +486,7 @@ public async ValueTask DisposeAsync() { _flushLock.Dispose(); _shutdownSource.Dispose(); - LoopTask.Dispose(); + _loopTask.Dispose(); } } diff --git a/src/Sentry/Protocol/Metrics/Metric.cs b/src/Sentry/Protocol/Metrics/Metric.cs index 34ff3b7060..7a9a5ca737 100644 --- a/src/Sentry/Protocol/Metrics/Metric.cs +++ b/src/Sentry/Protocol/Metrics/Metric.cs @@ -100,10 +100,10 @@ public async Task SerializeAsync(Stream stream, IDiagnosticLogger? logger, Cance await Write($"{key}:SanitizeValue(value)"); } } -#pragma warning restore CA2007 - await Write($"|T{Timestamp.GetTimeBucketKey().ToString(CultureInfo.InvariantCulture)}\n").ConfigureAwait(false); + await Write($"|T{Timestamp.GetTimeBucketKey().ToString(CultureInfo.InvariantCulture)}\n"); return; +#pragma warning restore CA2007 async Task Write(string content) { diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index 2134d33511..c5035edfc0 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -255,7 +255,7 @@ internal void CaptureMetrics(IEnumerable metrics) /// internal void CaptureCodeLocations(CodeLocations codeLocations) { - _options.LogDebug($"Capturing code locations for period: {codeLocations.Timestamp}"); + _options.LogDebug("Capturing code locations for period: {0}", codeLocations.Timestamp); CaptureEnvelope(Envelope.FromCodeLocations(codeLocations)); } @@ -469,9 +469,16 @@ public void Dispose() { _options.LogDebug("Flushing SentryClient."); - Metrics.FlushAsync().GetAwaiter().GetResult(); - - // Worker should empty it's queue until SentryOptions.ShutdownTimeout - Worker.FlushAsync(_options.ShutdownTimeout).GetAwaiter().GetResult(); + try + { + Metrics.FlushAsync().ContinueWith(_ => + // Worker should empty it's queue until SentryOptions.ShutdownTimeout + Worker.FlushAsync(_options.ShutdownTimeout) + ).ConfigureAwait(false).GetAwaiter().GetResult(); + } + catch + { + _options.LogDebug("Failed to wait on metrics/worker to flush"); + } } } From bce6e50be4f6b2db5b024e21129995db3715d1cd Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Sun, 7 Jan 2024 12:30:57 +1300 Subject: [PATCH 52/52] Removed unused private field --- src/Sentry/MetricAggregator.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index a2f8ac0740..82de196960 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -161,8 +161,6 @@ public void Timing(string key, DateTimeOffset? timestamp = null, int stackLevel = 1) => Emit(MetricType.Distribution, key, value, unit, tags, timestamp, stackLevel + 1); - private readonly object _emitLock = new object(); - private void Emit( MetricType type, string key,