Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 |
84 changes: 84 additions & 0 deletions benchmarks/Sentry.Benchmarks/SentryMetricEmitterBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -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<KeyValuePair<string, object>> attributes = new List<KeyValuePair<string, object>>(1)
{
KeyValuePair.Create<string, object>("attribute.key", "attribute-value"),
};
_metrics.EmitGauge("sentry_benchmarks.sentry_trace_metrics_tests.gauge", 1, MeasurementUnit.Information.Bit, attributes);
}

[Benchmark]
public void EmitWithAttributes_Span()
{
ReadOnlySpan<KeyValuePair<string, object>> attributes =
[
KeyValuePair.Create<string, object>("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");
}
}
}
21 changes: 21 additions & 0 deletions src/Sentry/Internal/DefaultSentryMetricEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,27 @@ private protected override void CaptureMetric<T>(SentryMetricType type, string n
CaptureMetric(metric);
}

#if NET6_0_OR_GREATER
/// <inheritdoc />
private protected override void CaptureMetric<T>(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

/// <inheritdoc />
private protected override void CaptureMetric<T>(SentryMetric<T> metric) where T : struct
{
Expand Down
8 changes: 8 additions & 0 deletions src/Sentry/Internal/DisabledSentryMetricEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ private protected override void CaptureMetric<T>(SentryMetricType type, string n
// disabled
}

#if NET6_0_OR_GREATER
/// <inheritdoc />
private protected override void CaptureMetric<T>(SentryMetricType type, string name, T value, string? unit, in TagList attributes, Scope? scope) where T : struct
{
// disabled
}
#endif

/// <inheritdoc />
private protected override void CaptureMetric<T>(SentryMetric<T> metric) where T : struct
{
Expand Down
21 changes: 21 additions & 0 deletions src/Sentry/Protocol/SentryAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,27 @@ internal void SetAttributes(ReadOnlySpan<KeyValuePair<string, object>> 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

/// <inheritdoc cref="ISentryJsonSerializable.WriteTo(Utf8JsonWriter, IDiagnosticLogger)" />
public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger)
{
Expand Down
10 changes: 10 additions & 0 deletions src/Sentry/SentryMetric.Factory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ internal static SentryMetric<T> Create<T>(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<T> Create<T>(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<T>(hub, options, clock, type, name, value, unit, scope);
metric.Attributes.SetAttributes(in attributes);
return metric;
}
#endif

private static bool IsSupported<T>() where T : struct
{
var valueType = typeof(T);
Expand Down
17 changes: 17 additions & 0 deletions src/Sentry/SentryMetricEmitter.Counter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,21 @@ public void EmitCounter<T>(string name, T value, ReadOnlySpan<KeyValuePair<strin
{
CaptureMetric(SentryMetricType.Counter, name, value, null, attributes, scope);
}

#if NET6_0_OR_GREATER
/// <summary>
/// Increment a counter.
/// </summary>
/// <param name="name">The name of the metric.</param>
/// <param name="value">The value of the metric.</param>
/// <param name="attributes">A dictionary of attributes (key-value pairs with type information).</param>
/// <param name="scope">The scope to capture the metric with.</param>
/// <typeparam name="T">The numeric type of the metric.</typeparam>
/// <remarks>Supported numeric value types for <typeparamref name="T"/> are <see langword="byte"/>, <see langword="short"/>, <see langword="int"/>, <see langword="long"/>, <see langword="float"/>, and <see langword="double"/>.</remarks>
[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<T>(string name, T value, in TagList attributes, Scope? scope = null) where T : struct
{
CaptureMetric(SentryMetricType.Counter, name, value, null, in attributes, scope);
}
#endif
}
45 changes: 42 additions & 3 deletions src/Sentry/SentryMetricEmitter.Distribution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public void EmitDistribution<T>(string name, T value) where T : struct
/// <typeparam name="T">The numeric type of the metric.</typeparam>
/// <remarks>Supported numeric value types for <typeparamref name="T"/> are <see langword="byte"/>, <see langword="short"/>, <see langword="int"/>, <see langword="long"/>, <see langword="float"/>, and <see langword="double"/>.</remarks>
[Obsolete(ObsoleteStringUnitForwardCompatibility)]
[EditorBrowsable(EditorBrowsableState.Never)]
public void EmitDistribution<T>(string name, T value, string? unit) where T : struct
{
CaptureMetric(SentryMetricType.Distribution, name, value, unit, [], null);
Expand Down Expand Up @@ -64,12 +65,12 @@ public void EmitDistribution<T>(string name, T value, Scope? scope) where T : st
/// <typeparam name="T">The numeric type of the metric.</typeparam>
/// <remarks>Supported numeric value types for <typeparamref name="T"/> are <see langword="byte"/>, <see langword="short"/>, <see langword="int"/>, <see langword="long"/>, <see langword="float"/>, and <see langword="double"/>.</remarks>
[Obsolete(ObsoleteStringUnitForwardCompatibility)]
[EditorBrowsable(EditorBrowsableState.Never)]
public void EmitDistribution<T>(string name, T value, string? unit, Scope? scope) where T : struct
{
CaptureMetric(SentryMetricType.Distribution, name, value, unit, [], scope);
}


/// <summary>
/// Add a distribution value.
/// </summary>
Expand All @@ -84,8 +85,6 @@ public void EmitDistribution<T>(string name, T value, MeasurementUnit unit, Scop
CaptureMetric(SentryMetricType.Distribution, name, value, unit.ToNullableString(), [], scope);
}



/// <summary>
/// Add a distribution value.
/// </summary>
Expand All @@ -97,6 +96,7 @@ public void EmitDistribution<T>(string name, T value, MeasurementUnit unit, Scop
/// <typeparam name="T">The numeric type of the metric.</typeparam>
/// <remarks>Supported numeric value types for <typeparamref name="T"/> are <see langword="byte"/>, <see langword="short"/>, <see langword="int"/>, <see langword="long"/>, <see langword="float"/>, and <see langword="double"/>.</remarks>
[Obsolete(ObsoleteStringUnitForwardCompatibility)]
[EditorBrowsable(EditorBrowsableState.Never)]
public void EmitDistribution<T>(string name, T value, string? unit, IEnumerable<KeyValuePair<string, object>>? attributes, Scope? scope = null) where T : struct
{
CaptureMetric(SentryMetricType.Distribution, name, value, unit, attributes, scope);
Expand Down Expand Up @@ -128,6 +128,7 @@ public void EmitDistribution<T>(string name, T value, MeasurementUnit unit, IEnu
/// <typeparam name="T">The numeric type of the metric.</typeparam>
/// <remarks>Supported numeric value types for <typeparamref name="T"/> are <see langword="byte"/>, <see langword="short"/>, <see langword="int"/>, <see langword="long"/>, <see langword="float"/>, and <see langword="double"/>.</remarks>
[Obsolete(ObsoleteStringUnitForwardCompatibility)]
[EditorBrowsable(EditorBrowsableState.Never)]
public void EmitDistribution<T>(string name, T value, string? unit, ReadOnlySpan<KeyValuePair<string, object>> attributes, Scope? scope = null) where T : struct
{
CaptureMetric(SentryMetricType.Distribution, name, value, unit, attributes, scope);
Expand All @@ -147,4 +148,42 @@ public void EmitDistribution<T>(string name, T value, MeasurementUnit unit, Read
{
CaptureMetric(SentryMetricType.Distribution, name, value, unit.ToNullableString(), attributes, scope);
}

#if NET6_0_OR_GREATER
/// <summary>
/// Add a distribution value.
/// </summary>
/// <param name="name">The name of the metric.</param>
/// <param name="value">The value of the metric.</param>
/// <param name="unit">The unit of measurement.</param>
/// <param name="attributes">A dictionary of attributes (key-value pairs with type information).</param>
/// <param name="scope">The scope to capture the metric with.</param>
/// <typeparam name="T">The numeric type of the metric.</typeparam>
/// <remarks>Supported numeric value types for <typeparamref name="T"/> are <see langword="byte"/>, <see langword="short"/>, <see langword="int"/>, <see langword="long"/>, <see langword="float"/>, and <see langword="double"/>.</remarks>
[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<T>(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
/// <summary>
/// Add a distribution value.
/// </summary>
/// <param name="name">The name of the metric.</param>
/// <param name="value">The value of the metric.</param>
/// <param name="unit">The unit of measurement.</param>
/// <param name="attributes">A dictionary of attributes (key-value pairs with type information).</param>
/// <param name="scope">The scope to capture the metric with.</param>
/// <typeparam name="T">The numeric type of the metric.</typeparam>
/// <remarks>Supported numeric value types for <typeparamref name="T"/> are <see langword="byte"/>, <see langword="short"/>, <see langword="int"/>, <see langword="long"/>, <see langword="float"/>, and <see langword="double"/>.</remarks>
[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<T>(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
}
Loading
Loading