diff --git a/benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.SentryMetricEmitterBenchmarks-report-github.md b/benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.SentryMetricEmitterBenchmarks-report-github.md new file mode 100644 index 0000000000..b2fb7cadbe --- /dev/null +++ b/benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.SentryMetricEmitterBenchmarks-report-github.md @@ -0,0 +1,16 @@ +``` + +BenchmarkDotNet v0.13.12, macOS 26.4.1 (25E253) [Darwin 25.4.0] +Apple M3 Pro, 1 CPU, 12 logical and 12 physical cores +.NET SDK 10.0.203 + [Host] : .NET 9.0.8 (9.0.825.36511), Arm64 RyuJIT AdvSIMD + DefaultJob : .NET 9.0.8 (9.0.825.36511), Arm64 RyuJIT AdvSIMD + + +``` +| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +|------------------------------ |----------:|---------:|---------:|-------:|-------:|----------:| +| EmitWithoutAttributes | 99.82 ns | 1.894 ns | 1.945 ns | 0.0640 | 0.0001 | 536 B | +| EmitWithAttributes_Enumerable | 148.19 ns | 0.544 ns | 0.454 ns | 0.0851 | - | 712 B | +| EmitWithAttributes_Span | 127.01 ns | 0.300 ns | 0.266 ns | 0.0706 | 0.0002 | 592 B | +| EmitWithAttributes_TagList | 134.64 ns | 1.935 ns | 1.715 ns | 0.0706 | 0.0002 | 592 B | diff --git a/benchmarks/Sentry.Benchmarks/SentryMetricEmitterBenchmarks.cs b/benchmarks/Sentry.Benchmarks/SentryMetricEmitterBenchmarks.cs new file mode 100644 index 0000000000..0a929e613c --- /dev/null +++ b/benchmarks/Sentry.Benchmarks/SentryMetricEmitterBenchmarks.cs @@ -0,0 +1,84 @@ +#nullable enable + +using BenchmarkDotNet.Attributes; +using Sentry.Extensibility; +using Sentry.Internal; +using Sentry.Testing; + +namespace Sentry.Benchmarks; + +public class SentryMetricEmitterBenchmarks +{ + private Hub _hub = null!; + private SentryMetricEmitter _metrics = null!; + + private SentryMetric? _lastMetric; + + [GlobalSetup] + public void Setup() + { + SentryOptions options = new() + { + Dsn = DsnSamples.ValidDsn, + EnableMetrics = true, + }; + options.SetBeforeSendMetric((SentryMetric metric) => + { + _lastMetric = metric; + return null; + }); + + MockClock clock = new(new DateTimeOffset(2025, 04, 22, 14, 51, 00, 789, TimeSpan.FromHours(2))); + + _hub = new Hub(options, DisabledHub.Instance); + _metrics = SentryMetricEmitter.Create(_hub, options, clock); + } + + [Benchmark] + public void EmitWithoutAttributes() + { + _metrics.EmitGauge("sentry_benchmarks.sentry_trace_metrics_tests.gauge", 1); + } + + [Benchmark] + public void EmitWithAttributes_Enumerable() + { + IEnumerable> attributes = new List>(1) + { + KeyValuePair.Create("attribute.key", "attribute-value"), + }; + _metrics.EmitGauge("sentry_benchmarks.sentry_trace_metrics_tests.gauge", 1, MeasurementUnit.Information.Bit, attributes); + } + + [Benchmark] + public void EmitWithAttributes_Span() + { + ReadOnlySpan> attributes = + [ + KeyValuePair.Create("attribute.key", "attribute-value"), + ]; + _metrics.EmitGauge("sentry_benchmarks.sentry_trace_metrics_tests.gauge", 1, MeasurementUnit.Information.Bit, attributes); + } + + [Benchmark] + public void EmitWithAttributes_TagList() + { + TagList attributes = new() + { + { "attribute.key", "attribute-value" }, + }; + _metrics.EmitGauge("sentry_benchmarks.sentry_trace_metrics_tests.gauge", 1, MeasurementUnit.Information.Bit, in attributes); + } + + [GlobalCleanup] + public void Cleanup() + { + (_metrics as IDisposable)?.Dispose(); + _hub.Dispose(); + + if (_lastMetric is null) + { + throw new InvalidOperationException("Last Metric is null"); + } + } +} diff --git a/src/Sentry/Internal/DefaultSentryMetricEmitter.cs b/src/Sentry/Internal/DefaultSentryMetricEmitter.cs index 1b798b49b1..5f2f8fe427 100644 --- a/src/Sentry/Internal/DefaultSentryMetricEmitter.cs +++ b/src/Sentry/Internal/DefaultSentryMetricEmitter.cs @@ -61,6 +61,27 @@ private protected override void CaptureMetric(SentryMetricType type, string n CaptureMetric(metric); } +#if NET6_0_OR_GREATER + /// + private protected override void CaptureMetric(SentryMetricType type, string name, T value, string? unit, in TagList attributes, Scope? scope) where T : struct + { + if (!SentryMetric.IsSupported(typeof(T))) + { + _options.DiagnosticLogger?.LogWarning("{0} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, and double.", typeof(T)); + return; + } + + if (string.IsNullOrEmpty(name)) + { + _options.DiagnosticLogger?.LogWarning("Name of metrics cannot be null or empty. Metric-Type: {0}; Value-Type: {1}", type.ToString(), typeof(T)); + return; + } + + var metric = SentryMetric.Create(_hub, _options, _clock, type, name, value, unit, in attributes, scope); + CaptureMetric(metric); + } +#endif + /// private protected override void CaptureMetric(SentryMetric metric) where T : struct { diff --git a/src/Sentry/Internal/DisabledSentryMetricEmitter.cs b/src/Sentry/Internal/DisabledSentryMetricEmitter.cs index 1afef5e90b..1215999976 100644 --- a/src/Sentry/Internal/DisabledSentryMetricEmitter.cs +++ b/src/Sentry/Internal/DisabledSentryMetricEmitter.cs @@ -20,6 +20,14 @@ private protected override void CaptureMetric(SentryMetricType type, string n // disabled } +#if NET6_0_OR_GREATER + /// + private protected override void CaptureMetric(SentryMetricType type, string name, T value, string? unit, in TagList attributes, Scope? scope) where T : struct + { + // disabled + } +#endif + /// private protected override void CaptureMetric(SentryMetric metric) where T : struct { diff --git a/src/Sentry/Protocol/SentryAttributes.cs b/src/Sentry/Protocol/SentryAttributes.cs index 711860e480..ec8d7e9211 100644 --- a/src/Sentry/Protocol/SentryAttributes.cs +++ b/src/Sentry/Protocol/SentryAttributes.cs @@ -155,6 +155,27 @@ internal void SetAttributes(ReadOnlySpan> attribute } } +#if NET6_0_OR_GREATER + [SuppressMessage("Roslynator", "RCS1242:Do not pass non-read-only struct by read-only reference", Justification = $"Ensure that only readonly instance members of {nameof(TagList)} are invoked, to avoid a defensive copy created by the compiler.")] + internal void SetAttributes(in TagList attributes) + { + if (attributes.Count == 0) + { + return; + } + +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + _ = EnsureCapacity(Count + attributes.Count); +#endif + + for (var index = 0; index < attributes.Count; index++) + { + var attribute = attributes[index]; + this[attribute.Key] = new SentryAttribute(attribute.Value!); + } + } +#endif + /// public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) { diff --git a/src/Sentry/SentryMetric.Factory.cs b/src/Sentry/SentryMetric.Factory.cs index 7c9621ce84..245753fb3e 100644 --- a/src/Sentry/SentryMetric.Factory.cs +++ b/src/Sentry/SentryMetric.Factory.cs @@ -39,6 +39,16 @@ internal static SentryMetric Create(IHub hub, SentryOptions options, ISyst return metric; } +#if NET6_0_OR_GREATER + [SuppressMessage("Roslynator", "RCS1242:Do not pass non-read-only struct by read-only reference", Justification = $"Ensure that only readonly instance members of {nameof(TagList)} are invoked, to avoid a defensive copy created by the compiler.")] + internal static SentryMetric Create(IHub hub, SentryOptions options, ISystemClock clock, SentryMetricType type, string name, T value, string? unit, in TagList attributes, Scope? scope) where T : struct + { + var metric = CreateCore(hub, options, clock, type, name, value, unit, scope); + metric.Attributes.SetAttributes(in attributes); + return metric; + } +#endif + private static bool IsSupported() where T : struct { var valueType = typeof(T); diff --git a/src/Sentry/SentryMetricEmitter.Counter.cs b/src/Sentry/SentryMetricEmitter.Counter.cs index 74dd577a5c..4f2f92109a 100644 --- a/src/Sentry/SentryMetricEmitter.Counter.cs +++ b/src/Sentry/SentryMetricEmitter.Counter.cs @@ -54,4 +54,21 @@ public void EmitCounter(string name, T value, ReadOnlySpan + /// Increment a counter. + /// + /// The name of the metric. + /// The value of the metric. + /// A dictionary of attributes (key-value pairs with type information). + /// The scope to capture the metric with. + /// The numeric type of the metric. + /// Supported numeric value types for are , , , , , and . + [SuppressMessage("Roslynator", "RCS1242:Do not pass non-read-only struct by read-only reference", Justification = $"Ensure that only readonly instance members of {nameof(TagList)} are invoked, to avoid a defensive copy created by the compiler.")] + public void EmitCounter(string name, T value, in TagList attributes, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Counter, name, value, null, in attributes, scope); + } +#endif } diff --git a/src/Sentry/SentryMetricEmitter.Distribution.cs b/src/Sentry/SentryMetricEmitter.Distribution.cs index 862dd6ce15..7a3d8f9291 100644 --- a/src/Sentry/SentryMetricEmitter.Distribution.cs +++ b/src/Sentry/SentryMetricEmitter.Distribution.cs @@ -23,6 +23,7 @@ public void EmitDistribution(string name, T value) where T : struct /// The numeric type of the metric. /// Supported numeric value types for are , , , , , and . [Obsolete(ObsoleteStringUnitForwardCompatibility)] + [EditorBrowsable(EditorBrowsableState.Never)] public void EmitDistribution(string name, T value, string? unit) where T : struct { CaptureMetric(SentryMetricType.Distribution, name, value, unit, [], null); @@ -64,12 +65,12 @@ public void EmitDistribution(string name, T value, Scope? scope) where T : st /// The numeric type of the metric. /// Supported numeric value types for are , , , , , and . [Obsolete(ObsoleteStringUnitForwardCompatibility)] + [EditorBrowsable(EditorBrowsableState.Never)] public void EmitDistribution(string name, T value, string? unit, Scope? scope) where T : struct { CaptureMetric(SentryMetricType.Distribution, name, value, unit, [], scope); } - /// /// Add a distribution value. /// @@ -84,8 +85,6 @@ public void EmitDistribution(string name, T value, MeasurementUnit unit, Scop CaptureMetric(SentryMetricType.Distribution, name, value, unit.ToNullableString(), [], scope); } - - /// /// Add a distribution value. /// @@ -97,6 +96,7 @@ public void EmitDistribution(string name, T value, MeasurementUnit unit, Scop /// The numeric type of the metric. /// Supported numeric value types for are , , , , , and . [Obsolete(ObsoleteStringUnitForwardCompatibility)] + [EditorBrowsable(EditorBrowsableState.Never)] public void EmitDistribution(string name, T value, string? unit, IEnumerable>? attributes, Scope? scope = null) where T : struct { CaptureMetric(SentryMetricType.Distribution, name, value, unit, attributes, scope); @@ -128,6 +128,7 @@ public void EmitDistribution(string name, T value, MeasurementUnit unit, IEnu /// The numeric type of the metric. /// Supported numeric value types for are , , , , , and . [Obsolete(ObsoleteStringUnitForwardCompatibility)] + [EditorBrowsable(EditorBrowsableState.Never)] public void EmitDistribution(string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope = null) where T : struct { CaptureMetric(SentryMetricType.Distribution, name, value, unit, attributes, scope); @@ -147,4 +148,42 @@ public void EmitDistribution(string name, T value, MeasurementUnit unit, Read { CaptureMetric(SentryMetricType.Distribution, name, value, unit.ToNullableString(), attributes, scope); } + +#if NET6_0_OR_GREATER + /// + /// Add a distribution value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// A dictionary of attributes (key-value pairs with type information). + /// The scope to capture the metric with. + /// The numeric type of the metric. + /// Supported numeric value types for are , , , , , and . + [Obsolete(ObsoleteStringUnitForwardCompatibility)] + [EditorBrowsable(EditorBrowsableState.Never)] + [SuppressMessage("Roslynator", "RCS1242:Do not pass non-read-only struct by read-only reference", Justification = $"Ensure that only readonly instance members of {nameof(TagList)} are invoked, to avoid a defensive copy created by the compiler.")] + public void EmitDistribution(string name, T value, string? unit, in TagList attributes, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Distribution, name, value, unit, in attributes, scope); + } +#endif + +#if NET6_0_OR_GREATER + /// + /// Add a distribution value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// A dictionary of attributes (key-value pairs with type information). + /// The scope to capture the metric with. + /// The numeric type of the metric. + /// Supported numeric value types for are , , , , , and . + [SuppressMessage("Roslynator", "RCS1242:Do not pass non-read-only struct by read-only reference", Justification = $"Ensure that only readonly instance members of {nameof(TagList)} are invoked, to avoid a defensive copy created by the compiler.")] + public void EmitDistribution(string name, T value, MeasurementUnit unit, in TagList attributes, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Distribution, name, value, unit.ToNullableString(), in attributes, scope); + } +#endif } diff --git a/src/Sentry/SentryMetricEmitter.Gauge.cs b/src/Sentry/SentryMetricEmitter.Gauge.cs index 5bafa4e242..a04b107cd7 100644 --- a/src/Sentry/SentryMetricEmitter.Gauge.cs +++ b/src/Sentry/SentryMetricEmitter.Gauge.cs @@ -23,6 +23,7 @@ public void EmitGauge(string name, T value) where T : struct /// The numeric type of the metric. /// Supported numeric value types for are , , , , , and . [Obsolete(ObsoleteStringUnitForwardCompatibility)] + [EditorBrowsable(EditorBrowsableState.Never)] public void EmitGauge(string name, T value, string? unit) where T : struct { CaptureMetric(SentryMetricType.Gauge, name, value, unit, [], null); @@ -64,6 +65,7 @@ public void EmitGauge(string name, T value, Scope? scope) where T : struct /// The numeric type of the metric. /// Supported numeric value types for are , , , , , and . [Obsolete(ObsoleteStringUnitForwardCompatibility)] + [EditorBrowsable(EditorBrowsableState.Never)] public void EmitGauge(string name, T value, string? unit, Scope? scope) where T : struct { CaptureMetric(SentryMetricType.Gauge, name, value, unit, [], scope); @@ -94,6 +96,7 @@ public void EmitGauge(string name, T value, MeasurementUnit unit, Scope? scop /// The numeric type of the metric. /// Supported numeric value types for are , , , , , and . [Obsolete(ObsoleteStringUnitForwardCompatibility)] + [EditorBrowsable(EditorBrowsableState.Never)] public void EmitGauge(string name, T value, string? unit, IEnumerable>? attributes, Scope? scope = null) where T : struct { CaptureMetric(SentryMetricType.Gauge, name, value, unit, attributes, scope); @@ -125,6 +128,7 @@ public void EmitGauge(string name, T value, MeasurementUnit unit, IEnumerable /// The numeric type of the metric. /// Supported numeric value types for are , , , , , and . [Obsolete(ObsoleteStringUnitForwardCompatibility)] + [EditorBrowsable(EditorBrowsableState.Never)] public void EmitGauge(string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope = null) where T : struct { CaptureMetric(SentryMetricType.Gauge, name, value, unit, attributes, scope); @@ -144,4 +148,42 @@ public void EmitGauge(string name, T value, MeasurementUnit unit, ReadOnlySpa { CaptureMetric(SentryMetricType.Gauge, name, value, unit.ToNullableString(), attributes, scope); } + +#if NET6_0_OR_GREATER + /// + /// Set a gauge value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// A dictionary of attributes (key-value pairs with type information). + /// The scope to capture the metric with. + /// The numeric type of the metric. + /// Supported numeric value types for are , , , , , and . + [Obsolete(ObsoleteStringUnitForwardCompatibility)] + [EditorBrowsable(EditorBrowsableState.Never)] + [SuppressMessage("Roslynator", "RCS1242:Do not pass non-read-only struct by read-only reference", Justification = $"Ensure that only readonly instance members of {nameof(TagList)} are invoked, to avoid a defensive copy created by the compiler.")] + public void EmitGauge(string name, T value, string? unit, in TagList attributes, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Gauge, name, value, unit, in attributes, scope); + } +#endif + +#if NET6_0_OR_GREATER + /// + /// Set a gauge value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// A dictionary of attributes (key-value pairs with type information). + /// The scope to capture the metric with. + /// The numeric type of the metric. + /// Supported numeric value types for are , , , , , and . + [SuppressMessage("Roslynator", "RCS1242:Do not pass non-read-only struct by read-only reference", Justification = $"Ensure that only readonly instance members of {nameof(TagList)} are invoked, to avoid a defensive copy created by the compiler.")] + public void EmitGauge(string name, T value, MeasurementUnit unit, in TagList attributes, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Gauge, name, value, unit.ToNullableString(), in attributes, scope); + } +#endif } diff --git a/src/Sentry/SentryMetricEmitter.cs b/src/Sentry/SentryMetricEmitter.cs index d3f8aa194c..b4ce10521c 100644 --- a/src/Sentry/SentryMetricEmitter.cs +++ b/src/Sentry/SentryMetricEmitter.cs @@ -48,6 +48,21 @@ private protected SentryMetricEmitter() /// The numeric type of the metric. private protected abstract void CaptureMetric(SentryMetricType type, string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope) where T : struct; +#if NET6_0_OR_GREATER + /// + /// Buffers a Sentry Metric item + /// via the associated Batch Processor. + /// + /// The type of metric. + /// The name of the metric. + /// The numeric value of the metric. + /// The unit of measurement for the metric value. + /// A dictionary of key-value pairs of arbitrary data attached to the metric. + /// The optional scope to capture the metric with. + /// The numeric type of the metric. + private protected abstract void CaptureMetric(SentryMetricType type, string name, T value, string? unit, in TagList attributes, Scope? scope) where T : struct; +#endif + /// /// Buffers a Sentry Metric item /// via the associated Batch Processor. diff --git a/test/Sentry.Testing/InMemorySentryMetricEmitter.cs b/test/Sentry.Testing/InMemorySentryMetricEmitter.cs index ecc5e575fa..6678b4d4f5 100644 --- a/test/Sentry.Testing/InMemorySentryMetricEmitter.cs +++ b/test/Sentry.Testing/InMemorySentryMetricEmitter.cs @@ -19,6 +19,14 @@ private protected override void CaptureMetric(SentryMetricType type, string n Entries.Add(MetricEntry.Create(type, name, value, unit, attributes.ToArray(), scope)); } +#if NET6_0_OR_GREATER + /// + private protected override void CaptureMetric(SentryMetricType type, string name, T value, string? unit, in TagList attributes, Scope? scope) where T : struct + { + Entries.Add(MetricEntry.Create(type, name, value, unit, attributes, scope)); + } +#endif + /// private protected override void CaptureMetric(SentryMetric metric) where T : struct { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt index 5b0c537022..21b657c709 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -688,6 +688,8 @@ namespace Sentry where T : struct { } public void EmitCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } + public void EmitCounter(string name, T value, in System.Diagnostics.TagList attributes, Sentry.Scope? scope = null) + where T : struct { } public void EmitDistribution(string name, T value) where T : struct { } public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit) @@ -710,6 +712,8 @@ namespace Sentry where T : struct { } public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } + public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit, in System.Diagnostics.TagList attributes, Sentry.Scope? scope = null) + where T : struct { } [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + "d.")] @@ -720,6 +724,11 @@ namespace Sentry "d.")] public void EmitDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] + public void EmitDistribution(string name, T value, string? unit, in System.Diagnostics.TagList attributes, Sentry.Scope? scope = null) + where T : struct { } public void EmitGauge(string name, T value) where T : struct { } public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit) @@ -742,6 +751,8 @@ namespace Sentry where T : struct { } public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } + public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit, in System.Diagnostics.TagList attributes, Sentry.Scope? scope = null) + where T : struct { } [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + "d.")] @@ -752,6 +763,11 @@ namespace Sentry "d.")] public void EmitGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] + public void EmitGauge(string name, T value, string? unit, in System.Diagnostics.TagList attributes, Sentry.Scope? scope = null) + where T : struct { } protected abstract void Flush(); } public enum SentryMetricType diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 5b0c537022..21b657c709 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -688,6 +688,8 @@ namespace Sentry where T : struct { } public void EmitCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } + public void EmitCounter(string name, T value, in System.Diagnostics.TagList attributes, Sentry.Scope? scope = null) + where T : struct { } public void EmitDistribution(string name, T value) where T : struct { } public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit) @@ -710,6 +712,8 @@ namespace Sentry where T : struct { } public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } + public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit, in System.Diagnostics.TagList attributes, Sentry.Scope? scope = null) + where T : struct { } [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + "d.")] @@ -720,6 +724,11 @@ namespace Sentry "d.")] public void EmitDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] + public void EmitDistribution(string name, T value, string? unit, in System.Diagnostics.TagList attributes, Sentry.Scope? scope = null) + where T : struct { } public void EmitGauge(string name, T value) where T : struct { } public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit) @@ -742,6 +751,8 @@ namespace Sentry where T : struct { } public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } + public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit, in System.Diagnostics.TagList attributes, Sentry.Scope? scope = null) + where T : struct { } [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + "d.")] @@ -752,6 +763,11 @@ namespace Sentry "d.")] public void EmitGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] + public void EmitGauge(string name, T value, string? unit, in System.Diagnostics.TagList attributes, Sentry.Scope? scope = null) + where T : struct { } protected abstract void Flush(); } public enum SentryMetricType diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index 5b0c537022..21b657c709 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -688,6 +688,8 @@ namespace Sentry where T : struct { } public void EmitCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } + public void EmitCounter(string name, T value, in System.Diagnostics.TagList attributes, Sentry.Scope? scope = null) + where T : struct { } public void EmitDistribution(string name, T value) where T : struct { } public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit) @@ -710,6 +712,8 @@ namespace Sentry where T : struct { } public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } + public void EmitDistribution(string name, T value, Sentry.MeasurementUnit unit, in System.Diagnostics.TagList attributes, Sentry.Scope? scope = null) + where T : struct { } [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + "d.")] @@ -720,6 +724,11 @@ namespace Sentry "d.")] public void EmitDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] + public void EmitDistribution(string name, T value, string? unit, in System.Diagnostics.TagList attributes, Sentry.Scope? scope = null) + where T : struct { } public void EmitGauge(string name, T value) where T : struct { } public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit) @@ -742,6 +751,8 @@ namespace Sentry where T : struct { } public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } + public void EmitGauge(string name, T value, Sentry.MeasurementUnit unit, in System.Diagnostics.TagList attributes, Sentry.Scope? scope = null) + where T : struct { } [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + "d.")] @@ -752,6 +763,11 @@ namespace Sentry "d.")] public void EmitGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) where T : struct { } + [System.Obsolete("Custom units may be supported in the future. The String-based overloads are for f" + + "orward compatibility. The MeasurementUnit-based overloads are currently preferre" + + "d.")] + public void EmitGauge(string name, T value, string? unit, in System.Diagnostics.TagList attributes, Sentry.Scope? scope = null) + where T : struct { } protected abstract void Flush(); } public enum SentryMetricType diff --git a/test/Sentry.Tests/Sentry.Tests.csproj b/test/Sentry.Tests/Sentry.Tests.csproj index 8e536bf1af..fd9b1876d8 100644 --- a/test/Sentry.Tests/Sentry.Tests.csproj +++ b/test/Sentry.Tests/Sentry.Tests.csproj @@ -39,7 +39,7 @@ $([System.String]::Copy('%(FileName)').Split('.')[0]) HintTests.cs - + SentryMetricEmitterTests.cs diff --git a/test/Sentry.Tests/SentryMetricEmitterTests.Attributes.cs b/test/Sentry.Tests/SentryMetricEmitterTests.Attributes.cs new file mode 100644 index 0000000000..6e28f36075 --- /dev/null +++ b/test/Sentry.Tests/SentryMetricEmitterTests.Attributes.cs @@ -0,0 +1,132 @@ +#nullable enable + +namespace Sentry.Tests; + +public partial class SentryMetricEmitterTests +{ + [Theory] + [InlineData(SentryMetricType.Counter)] + [InlineData(SentryMetricType.Gauge)] + [InlineData(SentryMetricType.Distribution)] + public void Emit_WithAttributes_Enumerable(SentryMetricType type) + { + SentryMetric? captured = null; + _fixture.Options.SetBeforeSendMetric(SentryMetric? (SentryMetric metric) => + { + captured = metric; + return null; + }); + var metrics = _fixture.GetSut(); + + IEnumerable>? attributes = [new KeyValuePair("attribute.key", "attribute-value")]; + metrics.Emit(type, 1, attributes); + + captured.Should().NotBeNull(); + captured.Attributes.ShouldContain("attribute.key", "attribute-value"); + } + + [Theory] + [InlineData(SentryMetricType.Counter)] + [InlineData(SentryMetricType.Gauge)] + [InlineData(SentryMetricType.Distribution)] + public void Emit_WithAttributes_Span(SentryMetricType type) + { + SentryMetric? captured = null; + _fixture.Options.SetBeforeSendMetric(SentryMetric? (SentryMetric metric) => + { + captured = metric; + return null; + }); + var metrics = _fixture.GetSut(); + + ReadOnlySpan> attributes = [new KeyValuePair("attribute.key", "attribute-value")]; + metrics.Emit(type, 1, attributes); + + captured.Should().NotBeNull(); + captured.Attributes.ShouldContain("attribute.key", "attribute-value"); + } + +#if NET6_0_OR_GREATER + [Theory] + [InlineData(SentryMetricType.Counter)] + [InlineData(SentryMetricType.Gauge)] + [InlineData(SentryMetricType.Distribution)] + public void Emit_WithAttributes_TagList(SentryMetricType type) + { + SentryMetric? captured = null; + _fixture.Options.SetBeforeSendMetric(SentryMetric? (SentryMetric metric) => + { + captured = metric; + return null; + }); + var metrics = _fixture.GetSut(); + + TagList attributes = new() { { "attribute.key", "attribute-value" } }; + metrics.Emit(type, 1, in attributes); + + captured.Should().NotBeNull(); + captured.Attributes.ShouldContain("attribute.key", "attribute-value"); + } +#endif +} + +[Obsolete(SentryMetricEmitter.ObsoleteStringUnitForwardCompatibility)] +file static class SentryMetricEmitterExtensions +{ + public static void Emit(this SentryMetricEmitter metrics, SentryMetricType type, T value, IEnumerable>? attributes) where T : struct + { + switch (type) + { + case SentryMetricType.Counter: + metrics.EmitCounter("sentry_tests.sentry_trace_metrics_tests.counter", value, attributes); + break; + case SentryMetricType.Gauge: + metrics.EmitGauge("sentry_tests.sentry_trace_metrics_tests.counter", value, "measurement_unit", attributes); + break; + case SentryMetricType.Distribution: + metrics.EmitDistribution("sentry_tests.sentry_trace_metrics_tests.counter", value, "measurement_unit", attributes); + break; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } + + public static void Emit(this SentryMetricEmitter metrics, SentryMetricType type, T value, ReadOnlySpan> attributes) where T : struct + { + switch (type) + { + case SentryMetricType.Counter: + metrics.EmitCounter("sentry_tests.sentry_trace_metrics_tests.counter", value, attributes); + break; + case SentryMetricType.Gauge: + metrics.EmitGauge("sentry_tests.sentry_trace_metrics_tests.counter", value, "measurement_unit", attributes); + break; + case SentryMetricType.Distribution: + metrics.EmitDistribution("sentry_tests.sentry_trace_metrics_tests.counter", value, "measurement_unit", attributes); + break; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } + +#if NET6_0_OR_GREATER + [SuppressMessage("Roslynator", "RCS1242:Do not pass non-read-only struct by read-only reference", Justification = $"Ensure that only readonly instance members of {nameof(TagList)} are invoked, to avoid a defensive copy created by the compiler.")] + public static void Emit(this SentryMetricEmitter metrics, SentryMetricType type, T value, in TagList attributes) where T : struct + { + switch (type) + { + case SentryMetricType.Counter: + metrics.EmitCounter("sentry_tests.sentry_trace_metrics_tests.counter", value, in attributes); + break; + case SentryMetricType.Gauge: + metrics.EmitGauge("sentry_tests.sentry_trace_metrics_tests.counter", value, "measurement_unit", in attributes); + break; + case SentryMetricType.Distribution: + metrics.EmitDistribution("sentry_tests.sentry_trace_metrics_tests.counter", value, "measurement_unit", in attributes); + break; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } +#endif +}