From 6be1e0124c2c1349f4cc70bf46d1767c3d2b8c2f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stefan=20P=C3=B6lz?=
<38893694+Flash0ver@users.noreply.github.com>
Date: Fri, 9 Jan 2026 17:37:49 +0100
Subject: [PATCH 01/26] feat(metrics): Trace-connected Metrics (Implementation)
---
...chmarks.cs => BatchProcessorBenchmarks.cs} | 12 +-
....BatchProcessorBenchmarks-report-github.md | 24 ++
...gBatchProcessorBenchmarks-report-github.md | 24 --
src/Sentry/BindableSentryOptions.cs | 17 +-
src/Sentry/Extensibility/DisabledHub.cs | 7 +-
src/Sentry/Extensibility/HubAdapter.cs | 7 +-
src/Sentry/IHub.cs | 13 +
...cturedLogBatchBuffer.cs => BatchBuffer.cs} | 48 +--
...LogBatchProcessor.cs => BatchProcessor.cs} | 79 ++--
.../Internal/DefaultSentryStructuredLogger.cs | 5 +-
.../Internal/DefaultSentryTraceMetrics.cs | 76 ++++
.../Internal/DisabledSentryTraceMetrics.cs | 34 ++
src/Sentry/Internal/Hub.cs | 7 +-
src/Sentry/Protocol/Envelopes/Envelope.cs | 12 +
src/Sentry/Protocol/Envelopes/EnvelopeItem.cs | 13 +
src/Sentry/Protocol/StructuredLog.cs | 6 +
src/Sentry/Protocol/TraceMetric.cs | 43 +++
src/Sentry/SentryMetric.Factory.cs | 48 +++
src/Sentry/SentryMetric.Interface.cs | 12 +
src/Sentry/SentryMetric.cs | 357 ++++++++++++++++++
src/Sentry/SentryMetricType.cs | 54 +++
src/Sentry/SentryOptions.Callback.cs | 109 ++++++
src/Sentry/SentryOptions.cs | 49 ++-
src/Sentry/SentrySdk.cs | 26 ++
src/Sentry/SentryTraceMetrics.Public.cs | 124 ++++++
src/Sentry/SentryTraceMetrics.cs | 63 ++++
test/Sentry.Testing/BindableTests.cs | 1 +
.../InMemorySentryTraceMetrics.cs | 114 ++++++
...iApprovalTests.Run.DotNet10_0.verified.txt | 68 ++++
...piApprovalTests.Run.DotNet8_0.verified.txt | 68 ++++
...piApprovalTests.Run.DotNet9_0.verified.txt | 68 ++++
.../ApiApprovalTests.Run.Net4_8.verified.txt | 59 +++
...atchBufferTests.cs => BatchBufferTests.cs} | 43 ++-
...ocessorTests.cs => BatchProcessorTests.cs} | 13 +-
.../SentryStructuredLoggerTests.cs | 4 +-
35 files changed, 1585 insertions(+), 122 deletions(-)
rename benchmarks/Sentry.Benchmarks/{StructuredLogBatchProcessorBenchmarks.cs => BatchProcessorBenchmarks.cs} (71%)
create mode 100644 benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.BatchProcessorBenchmarks-report-github.md
delete mode 100644 benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.StructuredLogBatchProcessorBenchmarks-report-github.md
rename src/Sentry/Internal/{StructuredLogBatchBuffer.cs => BatchBuffer.cs} (83%)
rename src/Sentry/Internal/{StructuredLogBatchProcessor.cs => BatchProcessor.cs} (54%)
create mode 100644 src/Sentry/Internal/DefaultSentryTraceMetrics.cs
create mode 100644 src/Sentry/Internal/DisabledSentryTraceMetrics.cs
create mode 100644 src/Sentry/Protocol/TraceMetric.cs
create mode 100644 src/Sentry/SentryMetric.Factory.cs
create mode 100644 src/Sentry/SentryMetric.Interface.cs
create mode 100644 src/Sentry/SentryMetric.cs
create mode 100644 src/Sentry/SentryMetricType.cs
create mode 100644 src/Sentry/SentryOptions.Callback.cs
create mode 100644 src/Sentry/SentryTraceMetrics.Public.cs
create mode 100644 src/Sentry/SentryTraceMetrics.cs
create mode 100644 test/Sentry.Testing/InMemorySentryTraceMetrics.cs
rename test/Sentry.Tests/Internals/{StructuredLogBatchBufferTests.cs => BatchBufferTests.cs} (82%)
rename test/Sentry.Tests/Internals/{StructuredLogBatchProcessorTests.cs => BatchProcessorTests.cs} (91%)
diff --git a/benchmarks/Sentry.Benchmarks/StructuredLogBatchProcessorBenchmarks.cs b/benchmarks/Sentry.Benchmarks/BatchProcessorBenchmarks.cs
similarity index 71%
rename from benchmarks/Sentry.Benchmarks/StructuredLogBatchProcessorBenchmarks.cs
rename to benchmarks/Sentry.Benchmarks/BatchProcessorBenchmarks.cs
index 4a2d1fc981..37e1fcc411 100644
--- a/benchmarks/Sentry.Benchmarks/StructuredLogBatchProcessorBenchmarks.cs
+++ b/benchmarks/Sentry.Benchmarks/BatchProcessorBenchmarks.cs
@@ -1,13 +1,19 @@
using BenchmarkDotNet.Attributes;
using Sentry.Extensibility;
using Sentry.Internal;
+using Sentry.Protocol;
namespace Sentry.Benchmarks;
-public class StructuredLogBatchProcessorBenchmarks
+///
+/// (formerly "Sentry.Internal.StructuredLogBatchProcessor") was originally developed as Batch Processor for Logs only.
+/// When adding support for Trace-connected Metrics, which are quite similar to Logs, it has been made generic to support both.
+/// For comparability of results, we still benchmark with , rather than .
+///
+public class BatchProcessorBenchmarks
{
private Hub _hub;
- private StructuredLogBatchProcessor _batchProcessor;
+ private BatchProcessor _batchProcessor;
private SentryLog _log;
[Params(10, 100)]
@@ -29,7 +35,7 @@ public void Setup()
var clientReportRecorder = new NullClientReportRecorder();
_hub = new Hub(options, DisabledHub.Instance);
- _batchProcessor = new StructuredLogBatchProcessor(_hub, BatchCount, batchInterval, clientReportRecorder, null);
+ _batchProcessor = new BatchProcessor(_hub, BatchCount, batchInterval, StructuredLog.Capture, clientReportRecorder, null);
_log = new SentryLog(DateTimeOffset.Now, SentryId.Empty, SentryLogLevel.Trace, "message");
}
diff --git a/benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.BatchProcessorBenchmarks-report-github.md b/benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.BatchProcessorBenchmarks-report-github.md
new file mode 100644
index 0000000000..d298c9a815
--- /dev/null
+++ b/benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.BatchProcessorBenchmarks-report-github.md
@@ -0,0 +1,24 @@
+```
+
+BenchmarkDotNet v0.13.12, macOS 26.1 (25B78) [Darwin 25.1.0]
+Apple M3 Pro, 1 CPU, 12 logical and 12 physical cores
+.NET SDK 10.0.100
+ [Host] : .NET 8.0.14 (8.0.1425.11118), Arm64 RyuJIT AdvSIMD
+ DefaultJob : .NET 8.0.14 (8.0.1425.11118), Arm64 RyuJIT AdvSIMD
+
+
+```
+| Method | BatchCount | OperationsPerInvoke | Mean | Error | StdDev | Median | Gen0 | Allocated |
+|------------------------- |----------- |-------------------- |-------------:|------------:|-------------:|-------------:|-------:|----------:|
+| **EnqueueAndFlush** | **10** | **100** | **1,896.9 ns** | **9.94 ns** | **8.81 ns** | **1,894.2 ns** | **0.6104** | **5 KB** |
+| EnqueueAndFlush_Parallel | 10 | 100 | 16,520.9 ns | 327.78 ns | 746.51 ns | 16,350.4 ns | 1.1292 | 9.29 KB |
+| **EnqueueAndFlush** | **10** | **200** | **4,085.5 ns** | **80.03 ns** | **74.86 ns** | **4,087.1 ns** | **1.2207** | **10 KB** |
+| EnqueueAndFlush_Parallel | 10 | 200 | 39,371.8 ns | 776.85 ns | 1,360.59 ns | 38,725.0 ns | 1.6479 | 13.6 KB |
+| **EnqueueAndFlush** | **10** | **1000** | **18,829.3 ns** | **182.18 ns** | **142.24 ns** | **18,836.4 ns** | **6.1035** | **50 KB** |
+| EnqueueAndFlush_Parallel | 10 | 1000 | 151,934.1 ns | 2,631.83 ns | 3,232.12 ns | 151,495.9 ns | 3.6621 | 31.31 KB |
+| **EnqueueAndFlush** | **100** | **100** | **864.9 ns** | **2.16 ns** | **1.68 ns** | **865.0 ns** | **0.1469** | **1.2 KB** |
+| EnqueueAndFlush_Parallel | 100 | 100 | 7,414.9 ns | 74.86 ns | 70.02 ns | 7,405.9 ns | 0.5722 | 4.61 KB |
+| **EnqueueAndFlush** | **100** | **200** | **1,836.9 ns** | **15.28 ns** | **12.76 ns** | **1,834.9 ns** | **0.2937** | **2.41 KB** |
+| EnqueueAndFlush_Parallel | 100 | 200 | 37,119.5 ns | 726.04 ns | 1,252.39 ns | 36,968.9 ns | 0.8545 | 7.27 KB |
+| **EnqueueAndFlush** | **100** | **1000** | **8,567.2 ns** | **84.25 ns** | **74.68 ns** | **8,547.4 ns** | **1.4648** | **12.03 KB** |
+| EnqueueAndFlush_Parallel | 100 | 1000 | 255,284.5 ns | 5,095.08 ns | 12,593.77 ns | 258,313.9 ns | 1.9531 | 19.02 KB |
diff --git a/benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.StructuredLogBatchProcessorBenchmarks-report-github.md b/benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.StructuredLogBatchProcessorBenchmarks-report-github.md
deleted file mode 100644
index 8461170b2c..0000000000
--- a/benchmarks/Sentry.Benchmarks/BenchmarkDotNet.Artifacts/results/Sentry.Benchmarks.StructuredLogBatchProcessorBenchmarks-report-github.md
+++ /dev/null
@@ -1,24 +0,0 @@
-```
-
-BenchmarkDotNet v0.13.12, macOS 15.6 (24G84) [Darwin 24.6.0]
-Apple M3 Pro, 1 CPU, 12 logical and 12 physical cores
-.NET SDK 9.0.301
- [Host] : .NET 8.0.14 (8.0.1425.11118), Arm64 RyuJIT AdvSIMD
- DefaultJob : .NET 8.0.14 (8.0.1425.11118), Arm64 RyuJIT AdvSIMD
-
-
-```
-| Method | BatchCount | OperationsPerInvoke | Mean | Error | StdDev | Gen0 | Allocated |
-|------------------------- |----------- |-------------------- |-------------:|------------:|------------:|-------:|----------:|
-| **EnqueueAndFlush** | **10** | **100** | **1,793.4 ns** | **13.75 ns** | **12.86 ns** | **0.6104** | **5 KB** |
-| EnqueueAndFlush_Parallel | 10 | 100 | 18,550.8 ns | 368.24 ns | 889.34 ns | 1.1292 | 9.16 KB |
-| **EnqueueAndFlush** | **10** | **200** | **3,679.8 ns** | **18.65 ns** | **16.53 ns** | **1.2207** | **10 KB** |
-| EnqueueAndFlush_Parallel | 10 | 200 | 41,246.4 ns | 508.07 ns | 475.25 ns | 1.7090 | 14.04 KB |
-| **EnqueueAndFlush** | **10** | **1000** | **17,239.1 ns** | **62.50 ns** | **58.46 ns** | **6.1035** | **50 KB** |
-| EnqueueAndFlush_Parallel | 10 | 1000 | 192,059.3 ns | 956.92 ns | 895.11 ns | 4.3945 | 37.52 KB |
-| **EnqueueAndFlush** | **100** | **100** | **866.7 ns** | **1.99 ns** | **1.77 ns** | **0.1469** | **1.2 KB** |
-| EnqueueAndFlush_Parallel | 100 | 100 | 6,714.8 ns | 100.75 ns | 94.24 ns | 0.5569 | 4.52 KB |
-| **EnqueueAndFlush** | **100** | **200** | **1,714.5 ns** | **3.20 ns** | **3.00 ns** | **0.2937** | **2.41 KB** |
-| EnqueueAndFlush_Parallel | 100 | 200 | 43,842.8 ns | 860.74 ns | 1,718.99 ns | 0.9155 | 7.51 KB |
-| **EnqueueAndFlush** | **100** | **1000** | **8,537.8 ns** | **9.80 ns** | **9.17 ns** | **1.4648** | **12.03 KB** |
-| EnqueueAndFlush_Parallel | 100 | 1000 | 313,421.4 ns | 6,159.27 ns | 6,846.01 ns | 1.9531 | 18.37 KB |
diff --git a/src/Sentry/BindableSentryOptions.cs b/src/Sentry/BindableSentryOptions.cs
index bbadddaf33..52abdf7df9 100644
--- a/src/Sentry/BindableSentryOptions.cs
+++ b/src/Sentry/BindableSentryOptions.cs
@@ -1,7 +1,7 @@
namespace Sentry;
///
-/// Contains representations of the subset of properties in SentryOptions that can be set from ConfigurationBindings.
+/// Contains representations of the subset of properties in that can be set from ConfigurationBindings.
/// Note that all of these properties are nullable, so that if they are not present in configuration, the values from
/// the type being bound to will be preserved.
///
@@ -56,6 +56,8 @@ internal partial class BindableSentryOptions
public bool? EnableSpotlight { get; set; }
public string? SpotlightUrl { get; set; }
+ public ExperimentalSentryOptions? Experimental { get; set; }
+
public void ApplyTo(SentryOptions options)
{
options.IsGlobalModeEnabled = IsGlobalModeEnabled ?? options.IsGlobalModeEnabled;
@@ -106,6 +108,11 @@ public void ApplyTo(SentryOptions options)
options.EnableSpotlight = EnableSpotlight ?? options.EnableSpotlight;
options.SpotlightUrl = SpotlightUrl ?? options.SpotlightUrl;
+ if (Experimental is { } experimental)
+ {
+ options.Experimental.EnableMetrics = experimental.EnableMetrics ?? options.Experimental.EnableMetrics;
+ }
+
#if ANDROID
Android.ApplyTo(options.Android);
Native.ApplyTo(options.Native);
@@ -113,4 +120,12 @@ public void ApplyTo(SentryOptions options)
Native.ApplyTo(options.Native);
#endif
}
+
+ ///
+ /// Bindable Options for .
+ ///
+ internal class ExperimentalSentryOptions
+ {
+ public bool? EnableMetrics { get; set; }
+ }
}
diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs
index e8115c03f9..273fba0783 100644
--- a/src/Sentry/Extensibility/DisabledHub.cs
+++ b/src/Sentry/Extensibility/DisabledHub.cs
@@ -1,6 +1,5 @@
using Sentry.Internal;
using Sentry.Protocol.Envelopes;
-using Sentry.Protocol.Metrics;
namespace Sentry.Extensibility;
@@ -267,4 +266,10 @@ public void Dispose()
/// Disabled Logger.
///
public SentryStructuredLogger Logger => DisabledSentryStructuredLogger.Instance;
+
+ ///
+ /// Disabled Metrics.
+ ///
+ [Experimental("SENTRYTRACECONNECTEDMETRICS")]
+ public SentryTraceMetrics Metrics => DisabledSentryTraceMetrics.Instance;
}
diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs
index 45499369e6..f02b50b589 100644
--- a/src/Sentry/Extensibility/HubAdapter.cs
+++ b/src/Sentry/Extensibility/HubAdapter.cs
@@ -1,6 +1,5 @@
using Sentry.Infrastructure;
using Sentry.Protocol.Envelopes;
-using Sentry.Protocol.Metrics;
namespace Sentry.Extensibility;
@@ -37,6 +36,12 @@ private HubAdapter() { }
///
public SentryStructuredLogger Logger { [DebuggerStepThrough] get => SentrySdk.Logger; }
+ ///
+ /// Forwards the call to .
+ ///
+ [Experimental("SENTRYTRACECONNECTEDMETRICS")]
+ public SentryTraceMetrics Metrics { [DebuggerStepThrough] get => SentrySdk.Experimental.Metrics; }
+
///
/// Forwards the call to .
///
diff --git a/src/Sentry/IHub.cs b/src/Sentry/IHub.cs
index 076f454f3f..ee44057413 100644
--- a/src/Sentry/IHub.cs
+++ b/src/Sentry/IHub.cs
@@ -29,6 +29,19 @@ public interface IHub : ISentryClient, ISentryScopeManager
///
public SentryStructuredLogger Logger { get; }
+ ///
+ /// Generates and sends metrics to Sentry.
+ ///
+ ///
+ /// Available options:
+ ///
+ ///
+ ///
+ ///
+ ///
+ [Experimental("SENTRYTRACECONNECTEDMETRICS")]
+ public SentryTraceMetrics Metrics { get; }
+
///
/// Starts a transaction.
///
diff --git a/src/Sentry/Internal/StructuredLogBatchBuffer.cs b/src/Sentry/Internal/BatchBuffer.cs
similarity index 83%
rename from src/Sentry/Internal/StructuredLogBatchBuffer.cs
rename to src/Sentry/Internal/BatchBuffer.cs
index a9f49216b7..78ae1ab063 100644
--- a/src/Sentry/Internal/StructuredLogBatchBuffer.cs
+++ b/src/Sentry/Internal/BatchBuffer.cs
@@ -3,7 +3,7 @@
namespace Sentry.Internal;
///
-/// A wrapper over an , intended for reusable buffering.
+/// A wrapper over an , intended for reusable buffering for .
///
///
/// Must be attempted to flush via when either the is reached,
@@ -12,15 +12,15 @@ namespace Sentry.Internal;
/// allowing multiple threads for or exclusive access for .
///
[DebuggerDisplay("Name = {Name}, Capacity = {Capacity}, Additions = {_additions}, AddCount = {AddCount}, IsDisposed = {_disposed}")]
-internal sealed class StructuredLogBatchBuffer : IDisposable
+internal sealed class BatchBuffer : IDisposable
{
- private readonly SentryLog[] _array;
+ private readonly TItem[] _array;
private int _additions;
private readonly ScopedCountdownLock _addLock;
private readonly Timer _timer;
private readonly TimeSpan _timeout;
- private readonly Action _timeoutExceededAction;
+ private readonly Action> _timeoutExceededAction;
private volatile bool _disposed;
@@ -31,12 +31,12 @@ internal sealed class StructuredLogBatchBuffer : IDisposable
/// When the timeout exceeds after an item has been added and the not yet been exceeded, is invoked.
/// The operation to execute when the exceeds if the buffer is neither empty nor full.
/// Name of the new buffer.
- public StructuredLogBatchBuffer(int capacity, TimeSpan timeout, Action timeoutExceededAction, string? name = null)
+ public BatchBuffer(int capacity, TimeSpan timeout, Action> timeoutExceededAction, string? name = null)
{
ThrowIfLessThanTwo(capacity, nameof(capacity));
ThrowIfNegativeOrZero(timeout, nameof(timeout));
- _array = new SentryLog[capacity];
+ _array = new TItem[capacity];
_additions = 0;
_addLock = new ScopedCountdownLock();
@@ -72,18 +72,18 @@ public StructuredLogBatchBuffer(int capacity, TimeSpan timeout, Action
/// Element attempted to be added.
- /// An describing the result of the thread-safe operation.
- internal StructuredLogBatchBufferAddStatus Add(SentryLog item)
+ /// An describing the result of the thread-safe operation.
+ internal BatchBufferAddStatus Add(TItem item)
{
if (_disposed)
{
- return StructuredLogBatchBufferAddStatus.IgnoredIsDisposed;
+ return BatchBufferAddStatus.IgnoredIsDisposed;
}
using var scope = _addLock.TryEnterCounterScope();
if (!scope.IsEntered)
{
- return StructuredLogBatchBufferAddStatus.IgnoredIsFlushing;
+ return BatchBufferAddStatus.IgnoredIsFlushing;
}
var count = Interlocked.Increment(ref _additions);
@@ -92,24 +92,24 @@ internal StructuredLogBatchBufferAddStatus Add(SentryLog item)
{
EnableTimer();
_array[count - 1] = item;
- return StructuredLogBatchBufferAddStatus.AddedFirst;
+ return BatchBufferAddStatus.AddedFirst;
}
if (count < _array.Length)
{
_array[count - 1] = item;
- return StructuredLogBatchBufferAddStatus.Added;
+ return BatchBufferAddStatus.Added;
}
if (count == _array.Length)
{
DisableTimer();
_array[count - 1] = item;
- return StructuredLogBatchBufferAddStatus.AddedLast;
+ return BatchBufferAddStatus.AddedLast;
}
Debug.Assert(count > _array.Length);
- return StructuredLogBatchBufferAddStatus.IgnoredCapacityExceeded;
+ return BatchBufferAddStatus.IgnoredCapacityExceeded;
}
///
@@ -157,7 +157,7 @@ internal void OnIntervalElapsed(object? state)
/// Returns a new Array consisting of the elements successfully added.
///
/// An Array with Length of successful additions.
- private SentryLog[] ToArrayAndClear()
+ private TItem[] ToArrayAndClear()
{
var additions = _additions;
var length = _array.Length;
@@ -173,7 +173,7 @@ private SentryLog[] ToArrayAndClear()
///
/// The Length of the buffer a new Array is created from.
/// An Array with Length of .
- private SentryLog[] ToArrayAndClear(int length)
+ private TItem[] ToArrayAndClear(int length)
{
Debug.Assert(_addLock.IsSet);
@@ -182,14 +182,14 @@ private SentryLog[] ToArrayAndClear(int length)
return array;
}
- private SentryLog[] ToArray(int length)
+ private TItem[] ToArray(int length)
{
if (length == 0)
{
- return Array.Empty();
+ return Array.Empty();
}
- var array = new SentryLog[length];
+ var array = new TItem[length];
Array.Copy(_array, array, length);
return array;
}
@@ -253,17 +253,17 @@ static void ThrowNegativeOrZero(TimeSpan value, string paramName)
/// A scope than ensures only a single operation is in progress,
/// and blocks the calling thread until all operations have finished.
/// When is , no more can be started,
- /// which will then return immediately.
+ /// which will then return immediately.
///
///
/// Only when scope .
///
internal ref struct FlushScope : IDisposable
{
- private StructuredLogBatchBuffer? _lockObj;
+ private BatchBuffer? _lockObj;
private ScopedCountdownLock.LockScope _scope;
- internal FlushScope(StructuredLogBatchBuffer lockObj, ScopedCountdownLock.LockScope scope)
+ internal FlushScope(BatchBuffer lockObj, ScopedCountdownLock.LockScope scope)
{
Debug.Assert(scope.IsEntered);
_lockObj = lockObj;
@@ -272,7 +272,7 @@ internal FlushScope(StructuredLogBatchBuffer lockObj, ScopedCountdownLock.LockSc
internal bool IsEntered => _scope.IsEntered;
- internal SentryLog[] Flush()
+ internal TItem[] Flush()
{
var lockObj = _lockObj;
if (lockObj is not null)
@@ -300,7 +300,7 @@ public void Dispose()
}
}
-internal enum StructuredLogBatchBufferAddStatus : byte
+internal enum BatchBufferAddStatus : byte
{
AddedFirst,
Added,
diff --git a/src/Sentry/Internal/StructuredLogBatchProcessor.cs b/src/Sentry/Internal/BatchProcessor.cs
similarity index 54%
rename from src/Sentry/Internal/StructuredLogBatchProcessor.cs
rename to src/Sentry/Internal/BatchProcessor.cs
index 2fe5db924e..f71f2498e0 100644
--- a/src/Sentry/Internal/StructuredLogBatchProcessor.cs
+++ b/src/Sentry/Internal/BatchProcessor.cs
@@ -1,58 +1,59 @@
using Sentry.Extensibility;
-using Sentry.Protocol;
-using Sentry.Protocol.Envelopes;
namespace Sentry.Internal;
///
-/// The Batch Processor for Sentry Logs.
+/// The Sentry Batch Processor.
///
///
/// Uses a double buffer strategy to achieve synchronous and lock-free adding.
/// Switches the active buffer either when full or timeout exceeded (after first item added).
-/// Logs are dropped when both buffers are either full or being flushed.
-/// Logs are not enqueued when the Hub is disabled (Hub is being or has been disposed).
+/// Items are dropped when both buffers are either full or being flushed.
+/// Items are not enqueued when the Hub is disabled (Hub is being or has been disposed).
/// Flushing blocks the calling thread until all pending add operations have completed.
///
/// Implementation:
-/// - When Hub is disabled (i.e. disposed), does not enqueue log
-/// - Try to enqueue log into currently active buffer
-/// - when currently active buffer is full, try to enqueue log into the other buffer
-/// - when the other buffer is also full, or currently being flushed, then the log is dropped and a discarded event is recorded as a client report
+/// - When Hub is disabled (i.e. disposed), does not enqueue item
+/// - Try to enqueue item into currently active buffer
+/// - when currently active buffer is full, try to enqueue item into the other buffer
+/// - when the other buffer is also full, or currently being flushed, then the item is dropped and a discarded event is recorded as a client report
/// - Swap currently active buffer when
/// - buffer is full
/// - timeout has exceeded
-/// - Batch and Capture logs after swapping currently active buffer
+/// - Batch and Capture items after swapping currently active buffer
/// - wait until all pending add/enqueue operations have completed (required for timeout)
-/// - flush the buffer and capture an envelope containing the batched logs
-/// - After flush, logs can be enqueued again into the buffer
+/// - flush the buffer and capture an envelope containing the batched items
+/// - After flush, items can be enqueued again into the buffer
///
///
-/// Sentry Logs
/// Sentry Batch Processor
/// OpenTelemetry Batch Processor
-internal sealed class StructuredLogBatchProcessor : IDisposable
+/// Sentry Logs
+/// Sentry Metrics
+internal sealed class BatchProcessor : IDisposable
{
private readonly IHub _hub;
+ private readonly Action _sendAction;
private readonly IClientReportRecorder _clientReportRecorder;
private readonly IDiagnosticLogger? _diagnosticLogger;
- private readonly StructuredLogBatchBuffer _buffer1;
- private readonly StructuredLogBatchBuffer _buffer2;
- private volatile StructuredLogBatchBuffer _activeBuffer;
+ private readonly BatchBuffer _buffer1;
+ private readonly BatchBuffer _buffer2;
+ private volatile BatchBuffer _activeBuffer;
- public StructuredLogBatchProcessor(IHub hub, int batchCount, TimeSpan batchInterval, IClientReportRecorder clientReportRecorder, IDiagnosticLogger? diagnosticLogger)
+ public BatchProcessor(IHub hub, int batchCount, TimeSpan batchInterval, Action sendAction, IClientReportRecorder clientReportRecorder, IDiagnosticLogger? diagnosticLogger)
{
_hub = hub;
+ _sendAction = sendAction;
_clientReportRecorder = clientReportRecorder;
_diagnosticLogger = diagnosticLogger;
- _buffer1 = new StructuredLogBatchBuffer(batchCount, batchInterval, OnTimeoutExceeded, "Buffer 1");
- _buffer2 = new StructuredLogBatchBuffer(batchCount, batchInterval, OnTimeoutExceeded, "Buffer 2");
+ _buffer1 = new BatchBuffer(batchCount, batchInterval, OnTimeoutExceeded, "Buffer 1");
+ _buffer2 = new BatchBuffer(batchCount, batchInterval, OnTimeoutExceeded, "Buffer 2");
_activeBuffer = _buffer1;
}
- internal void Enqueue(SentryLog log)
+ internal void Enqueue(TItem item)
{
if (!_hub.IsEnabled)
{
@@ -61,21 +62,21 @@ internal void Enqueue(SentryLog log)
var activeBuffer = _activeBuffer;
- if (!TryEnqueue(activeBuffer, log))
+ if (!TryEnqueue(activeBuffer, item))
{
activeBuffer = ReferenceEquals(activeBuffer, _buffer1) ? _buffer2 : _buffer1;
- if (!TryEnqueue(activeBuffer, log))
+ if (!TryEnqueue(activeBuffer, item))
{
_clientReportRecorder.RecordDiscardedEvent(DiscardReason.Backpressure, DataCategory.Default, 1);
- _diagnosticLogger?.LogInfo("Log Buffer full ... dropping log");
+ _diagnosticLogger?.LogInfo("{0}-Buffer full ... dropping {0}", typeof(TItem).Name);
}
}
}
internal void Flush()
{
- CaptureLogs(_buffer1);
- CaptureLogs(_buffer2);
+ CaptureItems(_buffer1);
+ CaptureItems(_buffer2);
}
///
@@ -90,50 +91,50 @@ internal void OnIntervalElapsed()
activeBuffer.OnIntervalElapsed(activeBuffer);
}
- private bool TryEnqueue(StructuredLogBatchBuffer buffer, SentryLog log)
+ private bool TryEnqueue(BatchBuffer buffer, TItem item)
{
- var status = buffer.Add(log);
+ var status = buffer.Add(item);
- if (status is StructuredLogBatchBufferAddStatus.AddedLast)
+ if (status is BatchBufferAddStatus.AddedLast)
{
SwapActiveBuffer(buffer);
- CaptureLogs(buffer);
+ CaptureItems(buffer);
return true;
}
- return status is StructuredLogBatchBufferAddStatus.AddedFirst or StructuredLogBatchBufferAddStatus.Added;
+ return status is BatchBufferAddStatus.AddedFirst or BatchBufferAddStatus.Added;
}
- private void SwapActiveBuffer(StructuredLogBatchBuffer currentActiveBuffer)
+ private void SwapActiveBuffer(BatchBuffer currentActiveBuffer)
{
var newActiveBuffer = ReferenceEquals(currentActiveBuffer, _buffer1) ? _buffer2 : _buffer1;
_ = Interlocked.CompareExchange(ref _activeBuffer, newActiveBuffer, currentActiveBuffer);
}
- private void CaptureLogs(StructuredLogBatchBuffer buffer)
+ private void CaptureItems(BatchBuffer buffer)
{
- SentryLog[]? logs = null;
+ TItem[]? items = null;
using (var scope = buffer.TryEnterFlushScope())
{
if (scope.IsEntered)
{
- logs = scope.Flush();
+ items = scope.Flush();
}
}
- if (logs is not null && logs.Length != 0)
+ if (items is not null && items.Length != 0)
{
- _ = _hub.CaptureEnvelope(Envelope.FromLog(new StructuredLog(logs)));
+ _sendAction(_hub, items);
}
}
- private void OnTimeoutExceeded(StructuredLogBatchBuffer buffer)
+ private void OnTimeoutExceeded(BatchBuffer buffer)
{
if (!buffer.IsEmpty)
{
SwapActiveBuffer(buffer);
- CaptureLogs(buffer);
+ CaptureItems(buffer);
}
}
diff --git a/src/Sentry/Internal/DefaultSentryStructuredLogger.cs b/src/Sentry/Internal/DefaultSentryStructuredLogger.cs
index 3e42cc7005..9d4883f716 100644
--- a/src/Sentry/Internal/DefaultSentryStructuredLogger.cs
+++ b/src/Sentry/Internal/DefaultSentryStructuredLogger.cs
@@ -1,5 +1,6 @@
using Sentry.Extensibility;
using Sentry.Infrastructure;
+using Sentry.Protocol;
namespace Sentry.Internal;
@@ -9,7 +10,7 @@ internal sealed class DefaultSentryStructuredLogger : SentryStructuredLogger, ID
private readonly SentryOptions _options;
private readonly ISystemClock _clock;
- private readonly StructuredLogBatchProcessor _batchProcessor;
+ private readonly BatchProcessor _batchProcessor;
internal DefaultSentryStructuredLogger(IHub hub, SentryOptions options, ISystemClock clock, int batchCount, TimeSpan batchInterval)
{
@@ -20,7 +21,7 @@ internal DefaultSentryStructuredLogger(IHub hub, SentryOptions options, ISystemC
_options = options;
_clock = clock;
- _batchProcessor = new StructuredLogBatchProcessor(hub, batchCount, batchInterval, _options.ClientReportRecorder, _options.DiagnosticLogger);
+ _batchProcessor = new BatchProcessor(hub, batchCount, batchInterval, StructuredLog.Capture, _options.ClientReportRecorder, _options.DiagnosticLogger);
}
///
diff --git a/src/Sentry/Internal/DefaultSentryTraceMetrics.cs b/src/Sentry/Internal/DefaultSentryTraceMetrics.cs
new file mode 100644
index 0000000000..89e3433c8d
--- /dev/null
+++ b/src/Sentry/Internal/DefaultSentryTraceMetrics.cs
@@ -0,0 +1,76 @@
+using Sentry.Extensibility;
+using Sentry.Infrastructure;
+using Sentry.Protocol;
+
+namespace Sentry.Internal;
+
+internal sealed class DefaultSentryTraceMetrics : SentryTraceMetrics, IDisposable
+{
+ private readonly IHub _hub;
+ private readonly SentryOptions _options;
+ private readonly ISystemClock _clock;
+
+ private readonly BatchProcessor _batchProcessor;
+
+ internal DefaultSentryTraceMetrics(IHub hub, SentryOptions options, ISystemClock clock, int batchCount, TimeSpan batchInterval)
+ {
+ Debug.Assert(hub.IsEnabled);
+ Debug.Assert(options.Experimental is { EnableMetrics: true });
+
+ _hub = hub;
+ _options = options;
+ _clock = clock;
+
+ _batchProcessor = new BatchProcessor(hub, batchCount, batchInterval, TraceMetric.Capture, _options.ClientReportRecorder, _options.DiagnosticLogger);
+ }
+
+ ///
+ private protected override void CaptureMetric(SentryMetricType type, string name, T value, string? unit, IEnumerable>? attributes, Scope? scope) where T : struct
+ {
+ var metric = SentryMetric.Create(_hub, _clock, type, name, value, unit, attributes, scope);
+ CaptureMetric(metric);
+ }
+
+ ///
+ private protected override void CaptureMetric(SentryMetricType type, string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope) where T : struct
+ {
+ var metric = SentryMetric.Create(_hub, _clock, type, name, value, unit, attributes, scope);
+ CaptureMetric(metric);
+ }
+
+ ///
+ protected internal override void CaptureMetric(SentryMetric metric) where T : struct
+ {
+ var configuredMetric = metric;
+
+ if (_options.Experimental.BeforeSendMetricInternal is { } beforeSendMetric)
+ {
+ try
+ {
+ configuredMetric = beforeSendMetric.Invoke(metric);
+ }
+ catch (Exception e)
+ {
+ _options.DiagnosticLogger?.LogError(e, "The BeforeSendMetric callback threw an exception. The Metric will be dropped.");
+ return;
+ }
+ }
+
+ if (configuredMetric is not null)
+ {
+ _batchProcessor.Enqueue(configuredMetric);
+ }
+ }
+
+ ///
+ protected internal override void Flush()
+ {
+ _batchProcessor.Flush();
+ }
+
+ ///
+ public void Dispose()
+ {
+ _batchProcessor.Dispose();
+ }
+}
diff --git a/src/Sentry/Internal/DisabledSentryTraceMetrics.cs b/src/Sentry/Internal/DisabledSentryTraceMetrics.cs
new file mode 100644
index 0000000000..750f6717c1
--- /dev/null
+++ b/src/Sentry/Internal/DisabledSentryTraceMetrics.cs
@@ -0,0 +1,34 @@
+namespace Sentry.Internal;
+
+internal sealed class DisabledSentryTraceMetrics : SentryTraceMetrics
+{
+ internal static DisabledSentryTraceMetrics Instance { get; } = new DisabledSentryTraceMetrics();
+
+ internal DisabledSentryTraceMetrics()
+ {
+ }
+
+ ///
+ private protected override void CaptureMetric(SentryMetricType type, string name, T value, string? unit, IEnumerable>? attributes, Scope? scope) where T : struct
+ {
+ // disabled
+ }
+
+ ///
+ private protected override void CaptureMetric(SentryMetricType type, string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope) where T : struct
+ {
+ // disabled
+ }
+
+ ///
+ protected internal override void CaptureMetric(SentryMetric metric) where T : struct
+ {
+ // disabled
+ }
+
+ ///
+ protected internal override void Flush()
+ {
+ // disabled
+ }
+}
diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs
index f44a288b33..dd789b9155 100644
--- a/src/Sentry/Internal/Hub.cs
+++ b/src/Sentry/Internal/Hub.cs
@@ -1,6 +1,5 @@
using Sentry.Extensibility;
using Sentry.Infrastructure;
-using Sentry.Integrations;
using Sentry.Internal.Extensions;
using Sentry.Protocol.Envelopes;
using Sentry.Protocol.Metrics;
@@ -82,6 +81,7 @@ internal Hub(
}
Logger = SentryStructuredLogger.Create(this, options, _clock);
+ Metrics = SentryTraceMetrics.Create(this, options, _clock);
#if MEMORY_DUMP_SUPPORTED
if (options.HeapDumpOptions is not null)
@@ -819,6 +819,7 @@ public async Task FlushAsync(TimeSpan timeout)
try
{
Logger.Flush();
+ Metrics.Flush();
await CurrentClient.FlushAsync(timeout).ConfigureAwait(false);
}
catch (Exception e)
@@ -853,7 +854,9 @@ public void Dispose()
#endif
Logger.Flush();
+ Metrics.Flush();
(Logger as IDisposable)?.Dispose(); // see Sentry.Internal.DefaultSentryStructuredLogger
+ (Metrics as IDisposable)?.Dispose(); // see Sentry.Internal.DefaultSentryTraceMetrics
try
{
@@ -884,4 +887,6 @@ public void Dispose()
public SentryId LastEventId => CurrentScope.LastEventId;
public SentryStructuredLogger Logger { get; }
+
+ public SentryTraceMetrics Metrics { get; }
}
diff --git a/src/Sentry/Protocol/Envelopes/Envelope.cs b/src/Sentry/Protocol/Envelopes/Envelope.cs
index e9b06cff00..e3e85da713 100644
--- a/src/Sentry/Protocol/Envelopes/Envelope.cs
+++ b/src/Sentry/Protocol/Envelopes/Envelope.cs
@@ -446,6 +446,18 @@ internal static Envelope FromLog(StructuredLog log)
return new Envelope(header, items);
}
+ internal static Envelope FromMetric(TraceMetric metric)
+ {
+ var header = DefaultHeader;
+
+ var items = new[]
+ {
+ EnvelopeItem.FromMetric(metric),
+ };
+
+ return new Envelope(header, items);
+ }
+
private static async Task> DeserializeHeaderAsync(
Stream stream,
CancellationToken cancellationToken = default)
diff --git a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs
index 976c9a1305..21ab3ba25d 100644
--- a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs
+++ b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs
@@ -25,6 +25,7 @@ public sealed class EnvelopeItem : ISerializable, IDisposable
internal const string TypeValueMetric = "statsd";
internal const string TypeValueCodeLocations = "metric_meta";
internal const string TypeValueLog = "log";
+ internal const string TypeValueTraceMetric = "trace_metric";
private const string LengthKey = "length";
private const string FileNameKey = "filename";
@@ -369,6 +370,18 @@ internal static EnvelopeItem FromLog(StructuredLog log)
return new EnvelopeItem(header, new JsonSerializable(log));
}
+ internal static EnvelopeItem FromMetric(TraceMetric metric)
+ {
+ var header = new Dictionary(3, StringComparer.Ordinal)
+ {
+ [TypeKey] = TypeValueTraceMetric,
+ ["item_count"] = metric.Length,
+ ["content_type"] = "application/vnd.sentry.items.trace-metric+json",
+ };
+
+ return new EnvelopeItem(header, new JsonSerializable(metric));
+ }
+
private static async Task> DeserializeHeaderAsync(
Stream stream,
CancellationToken cancellationToken = default)
diff --git a/src/Sentry/Protocol/StructuredLog.cs b/src/Sentry/Protocol/StructuredLog.cs
index 6543d31ffc..6ff161e303 100644
--- a/src/Sentry/Protocol/StructuredLog.cs
+++ b/src/Sentry/Protocol/StructuredLog.cs
@@ -1,4 +1,5 @@
using Sentry.Extensibility;
+using Sentry.Protocol.Envelopes;
namespace Sentry.Protocol;
@@ -34,4 +35,9 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger)
writer.WriteEndArray();
writer.WriteEndObject();
}
+
+ internal static void Capture(IHub hub, SentryLog[] logs)
+ {
+ _ = hub.CaptureEnvelope(Envelope.FromLog(new StructuredLog(logs)));
+ }
}
diff --git a/src/Sentry/Protocol/TraceMetric.cs b/src/Sentry/Protocol/TraceMetric.cs
new file mode 100644
index 0000000000..cbcea00a57
--- /dev/null
+++ b/src/Sentry/Protocol/TraceMetric.cs
@@ -0,0 +1,43 @@
+using Sentry.Extensibility;
+using Sentry.Protocol.Envelopes;
+
+namespace Sentry.Protocol;
+
+///
+/// Represents the Sentry protocol for Trace-connected Metrics.
+///
+///
+/// Sentry Docs: .
+/// Sentry Developer Documentation: .
+///
+internal sealed class TraceMetric : ISentryJsonSerializable
+{
+ private readonly ISentryMetric[] _items;
+
+ public TraceMetric(ISentryMetric[] metrics)
+ {
+ _items = metrics;
+ }
+
+ public int Length => _items.Length;
+ public ReadOnlySpan Items => _items;
+
+ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger)
+ {
+ writer.WriteStartObject();
+ writer.WriteStartArray("items");
+
+ foreach (var metric in _items)
+ {
+ metric.WriteTo(writer, logger);
+ }
+
+ writer.WriteEndArray();
+ writer.WriteEndObject();
+ }
+
+ internal static void Capture(IHub hub, ISentryMetric[] metrics)
+ {
+ _ = hub.CaptureEnvelope(Envelope.FromMetric(new TraceMetric(metrics)));
+ }
+}
diff --git a/src/Sentry/SentryMetric.Factory.cs b/src/Sentry/SentryMetric.Factory.cs
new file mode 100644
index 0000000000..d72769e241
--- /dev/null
+++ b/src/Sentry/SentryMetric.Factory.cs
@@ -0,0 +1,48 @@
+using Sentry.Infrastructure;
+
+namespace Sentry;
+
+internal static class SentryMetric
+{
+ internal static SentryMetric Create(IHub hub, ISystemClock clock, SentryMetricType type, string name, T value, string? unit, IEnumerable>? attributes, Scope? scope) where T : struct
+ {
+ Debug.Assert(type is not SentryMetricType.Counter || unit is null, $"'{nameof(unit)}' is only used for Metrics of type {nameof(SentryMetricType.Gauge)} and {nameof(SentryMetricType.Distribution)}.");
+
+ var timestamp = clock.GetUtcNow();
+ SentryLog.GetTraceIdAndSpanId(hub, out var traceId, out var spanId); //TODO: move
+
+ var metric = new SentryMetric(timestamp, traceId, type, name, value)
+ {
+ SpanId = spanId,
+ Unit = unit,
+ };
+
+ scope ??= hub.GetScope();
+ metric.Apply(scope);
+
+ metric.SetAttributes(attributes);
+
+ return metric;
+ }
+
+ internal static SentryMetric Create(IHub hub, ISystemClock clock, SentryMetricType type, string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope) where T : struct
+ {
+ Debug.Assert(type is not SentryMetricType.Counter || unit is null, $"'{nameof(unit)}' is only used for Metrics of type {nameof(SentryMetricType.Gauge)} and {nameof(SentryMetricType.Distribution)}.");
+
+ var timestamp = clock.GetUtcNow();
+ SentryLog.GetTraceIdAndSpanId(hub, out var traceId, out var spanId);
+
+ var metric = new SentryMetric(timestamp, traceId, type, name, value)
+ {
+ SpanId = spanId,
+ Unit = unit,
+ };
+
+ scope ??= hub.GetScope();
+ metric.Apply(scope);
+
+ metric.SetAttributes(attributes);
+
+ return metric;
+ }
+}
diff --git a/src/Sentry/SentryMetric.Interface.cs b/src/Sentry/SentryMetric.Interface.cs
new file mode 100644
index 0000000000..8a91ad868a
--- /dev/null
+++ b/src/Sentry/SentryMetric.Interface.cs
@@ -0,0 +1,12 @@
+using Sentry.Extensibility;
+
+namespace Sentry;
+
+///
+/// Internal non-generic representation of .
+///
+internal interface ISentryMetric
+{
+ ///
+ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger);
+}
diff --git a/src/Sentry/SentryMetric.cs b/src/Sentry/SentryMetric.cs
new file mode 100644
index 0000000000..d7cbe39c34
--- /dev/null
+++ b/src/Sentry/SentryMetric.cs
@@ -0,0 +1,357 @@
+using Sentry.Extensibility;
+using Sentry.Protocol;
+
+namespace Sentry;
+
+///
+/// Represents a Sentry Trace-connected Metric.
+///
+/// The numeric type of the metric.
+///
+/// Sentry Docs: .
+/// Sentry Developer Documentation: .
+/// Sentry .NET SDK Docs: .
+///
+[DebuggerDisplay(@"SentryMetric \{ Type = {Type}, Name = '{Name}', Value = {Value} \}")]
+public sealed class SentryMetric : ISentryMetric where T : struct
+{
+ private Dictionary? _attributes;
+
+ [SetsRequiredMembers]
+ internal SentryMetric(DateTimeOffset timestamp, SentryId traceId, SentryMetricType type, string name, T value)
+ {
+ Timestamp = timestamp;
+ TraceId = traceId;
+ Type = type;
+ Name = name;
+ Value = value;
+ }
+
+ ///
+ /// Timestamp indicating when the metric was recorded.
+ ///
+ ///
+ /// Sent as seconds since the Unix epoch.
+ ///
+ public required DateTimeOffset Timestamp { get; init; }
+
+ ///
+ /// The trace id of the metric.
+ ///
+ ///
+ /// The value should be 16 random bytes encoded as a hex string (32 characters long).
+ /// The trace id should be grabbed from the current propagation context in the SDK.
+ ///
+ public required SentryId TraceId { get; init; }
+
+ ///
+ /// The type of metric.
+ ///
+ ///
+ /// One of:
+ ///
+ ///
+ /// Type
+ /// Description
+ ///
+ /// -
+ /// counter
+ /// A metric that increments counts.
+ ///
+ /// -
+ /// gauge
+ /// A metric that tracks a value that can go up or down.
+ ///
+ /// -
+ /// distribution
+ /// A metric that tracks the statistical distribution of values.
+ ///
+ ///
+ ///
+ public required SentryMetricType Type { get; init; }
+
+ ///
+ /// The name of the metric.
+ ///
+ ///
+ /// This should follow a hierarchical naming convention using dots as separators (e.g., api.response_time, db.query.duration).
+ ///
+ public required string Name { get; init; }
+
+ ///
+ /// The numeric value of the metric.
+ ///
+ ///
+ /// The interpretation depends on the metric type:
+ ///
+ ///
+ /// Type
+ /// Description
+ ///
+ /// -
+ /// counter
+ /// The count to increment by (should default to ).
+ ///
+ /// -
+ /// gauge
+ /// The current value.
+ ///
+ /// -
+ /// distribution
+ /// A single measured value.
+ ///
+ ///
+ ///
+ public required T Value { get; init; }
+
+ ///
+ /// The span id of the span that was active when the metric was emitted.
+ ///
+ ///
+ /// The value should be 8 random bytes encoded as a hex string (16 characters long).
+ /// The span id should be grabbed from the current active span in the SDK.
+ ///
+ public SpanId? SpanId { get; init; }
+
+ ///
+ /// The unit of measurement for the metric value.
+ ///
+ ///
+ /// Only used for and .
+ ///
+ public string? Unit { get; init; }
+
+ ///
+ /// A dictionary of key-value pairs of arbitrary data attached to the metric.
+ ///
+ ///
+ /// Attributes must also declare the type of the value.
+ /// Supported Types:
+ ///
+ ///
+ /// Type
+ /// Comment
+ ///
+ /// -
+ /// string
+ ///
+ ///
+ /// -
+ /// boolean
+ ///
+ ///
+ /// -
+ /// integer
+ /// 64-bit signed integer
+ ///
+ /// -
+ /// double
+ /// 64-bit floating point number
+ ///
+ ///
+ /// Integers should be 64-bit signed integers.
+ /// For 64-bit unsigned integers, use the string type to avoid overflow issues until unsigned integers are natively supported.
+ ///
+ public IReadOnlyDictionary Attributes
+ {
+ get
+ {
+#if NET8_0_OR_GREATER
+ return _attributes ?? (IReadOnlyDictionary)ReadOnlyDictionary.Empty;
+#else
+ return _attributes ?? EmptyAttributes;
+#endif
+ }
+ }
+
+ ///
+ /// Set a key-value pair of data attached to the metric.
+ ///
+ public void SetAttribute(string key, object value)
+ {
+ _attributes ??= new Dictionary();
+
+ _attributes[key] = new SentryAttribute(value);
+ }
+
+ internal void SetAttributes(IEnumerable>? attributes)
+ {
+ if (attributes is null)
+ {
+ return;
+ }
+
+ if (_attributes is null)
+ {
+ if (attributes.TryGetNonEnumeratedCount(out var count))
+ {
+ _attributes = new Dictionary(count);
+ }
+ else
+ {
+ _attributes = new Dictionary();
+ }
+ }
+ else
+ {
+#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+ if (attributes.TryGetNonEnumeratedCount(out var count))
+ {
+ _ = _attributes.EnsureCapacity(_attributes.Count + count);
+ }
+#endif
+ }
+
+ foreach (var attribute in attributes)
+ {
+ _attributes[attribute.Key] = attribute.Value;
+ }
+ }
+
+ internal void SetAttributes(ReadOnlySpan> attributes)
+ {
+ if (attributes.IsEmpty)
+ {
+ return;
+ }
+
+ if (_attributes is null)
+ {
+ _attributes = new Dictionary(attributes.Length);
+ }
+ else
+ {
+#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+ _ = _attributes.EnsureCapacity(_attributes.Count + attributes.Length);
+#endif
+ }
+
+ foreach (var attribute in attributes)
+ {
+ _attributes[attribute.Key] = attribute.Value;
+ }
+ }
+
+ internal void Apply(Scope? scope)
+ {
+ }
+
+ void ISentryMetric.WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger)
+ {
+ writer.WriteStartObject();
+
+#if NET9_0_OR_GREATER
+ writer.WriteNumber("timestamp", Timestamp.ToUnixTimeMilliseconds() / (double)TimeSpan.MillisecondsPerSecond);
+#else
+ writer.WriteNumber("timestamp", Timestamp.ToUnixTimeMilliseconds() / 1_000.0);
+#endif
+
+ writer.WriteString("type", Type.ToProtocolString(logger));
+ writer.WriteString("name", Name);
+ writer.WriteMetricValue("value", Value);
+
+ writer.WritePropertyName("trace_id");
+ TraceId.WriteTo(writer, logger);
+
+ if (SpanId.HasValue)
+ {
+ writer.WritePropertyName("span_id");
+ SpanId.Value.WriteTo(writer, logger);
+ }
+
+ if (Unit is not null)
+ {
+ writer.WriteString("unit", Unit);
+ }
+
+ if (_attributes is not null && _attributes.Count != 0)
+ {
+ writer.WritePropertyName("attributes");
+ writer.WriteStartObject();
+
+ foreach (var attribute in _attributes)
+ {
+ SentryAttributeSerializer.WriteAttribute(writer, attribute.Key, attribute.Value, logger);
+ }
+
+ writer.WriteEndObject();
+ }
+
+ writer.WriteEndObject();
+ }
+
+#if !NET8_0_OR_GREATER
+ private static IReadOnlyDictionary EmptyAttributes { get; } = new ReadOnlyDictionary(new Dictionary());
+#endif
+}
+
+// TODO: remove after upgrading 14.0 and updating
+#if !NET6_0_OR_GREATER
+file static class EnumerableExtensions
+{
+ internal static bool TryGetNonEnumeratedCount(this IEnumerable source, out int count)
+ {
+ if (source is null)
+ {
+ throw new ArgumentNullException(nameof(source));
+ }
+
+ if (source is ICollection genericCollection)
+ {
+ count = genericCollection.Count;
+ return true;
+ }
+
+ if (source is ICollection collection)
+ {
+ count = collection.Count;
+ return true;
+ }
+
+ count = 0;
+ return false;
+ }
+}
+#endif
+
+file static class Utf8JsonWriterExtensions
+{
+ //TODO: Integers should be a 64-bit signed integer, while doubles should be a 64-bit floating point number.
+ internal static void WriteMetricValue(this Utf8JsonWriter writer, string propertyName, T value) where T : struct
+ {
+ var type = typeof(T);
+
+ if (type == typeof(byte))
+ {
+ writer.WriteNumber(propertyName, (byte)(object)value);
+ }
+ else if (type == typeof(short))
+ {
+ writer.WriteNumber(propertyName, (short)(object)value);
+ }
+ else if (type == typeof(int))
+ {
+ writer.WriteNumber(propertyName, (int)(object)value);
+ }
+ else if (type == typeof(long))
+ {
+ writer.WriteNumber(propertyName, (long)(object)value);
+ }
+ else if (type == typeof(double))
+ {
+ writer.WriteNumber(propertyName, (double)(object)value);
+ }
+ else if (type == typeof(float))
+ {
+ writer.WriteNumber(propertyName, (float)(object)value);
+ }
+ else if (type == typeof(decimal))
+ {
+ writer.WriteNumber(propertyName, (decimal)(object)value);
+ }
+ else
+ {
+ Debug.Fail($"Unhandled Metric Type {typeof(T)}.", "This instruction should be unreachable.");
+ }
+ }
+}
diff --git a/src/Sentry/SentryMetricType.cs b/src/Sentry/SentryMetricType.cs
new file mode 100644
index 0000000000..df6601942c
--- /dev/null
+++ b/src/Sentry/SentryMetricType.cs
@@ -0,0 +1,54 @@
+using Sentry.Extensibility;
+
+namespace Sentry;
+
+///
+/// The type of metric.
+///
+public enum SentryMetricType
+{
+ ///
+ /// A metric that increments counts.
+ ///
+ ///
+ /// represents the count to increment by.
+ /// By default: .
+ ///
+ Counter,
+
+ ///
+ /// A metric that tracks a value that can go up or down.
+ ///
+ ///
+ /// represents the current value.
+ ///
+ Gauge,
+
+ ///
+ /// A metric that tracks the statistical distribution of values.
+ ///
+ ///
+ /// represents a single measured value.
+ ///
+ Distribution,
+}
+
+internal static class SentryMetricTypeExtensions
+{
+ internal static string ToProtocolString(this SentryMetricType type, IDiagnosticLogger? logger)
+ {
+ return type switch
+ {
+ SentryMetricType.Counter => "counter",
+ SentryMetricType.Gauge => "gauge",
+ SentryMetricType.Distribution => "distribution",
+ _ => IsNotDefined(type, logger),
+ };
+
+ static string IsNotDefined(SentryMetricType type, IDiagnosticLogger? logger)
+ {
+ logger?.LogDebug("Metric type {0} is not defined.", type);
+ return "unknown";
+ }
+ }
+}
diff --git a/src/Sentry/SentryOptions.Callback.cs b/src/Sentry/SentryOptions.Callback.cs
new file mode 100644
index 0000000000..3eea6eac21
--- /dev/null
+++ b/src/Sentry/SentryOptions.Callback.cs
@@ -0,0 +1,109 @@
+using Sentry.Extensibility;
+
+namespace Sentry;
+
+public partial class SentryOptions
+{
+#if NET6_0_OR_GREATER
+ ///
+ /// Similar to .
+ ///
+#endif
+ internal sealed class TraceMetricsCallbacks
+ {
+ //TODO: Integers should be a 64-bit signed integer, while doubles should be a 64-bit floating point number.
+ private Func, SentryMetric?> _beforeSendMetricInt32;
+ private Func, SentryMetric?> _beforeSendMetricByte;
+ private Func, SentryMetric?> _beforeSendMetricInt16;
+ private Func, SentryMetric?> _beforeSendMetricInt64;
+ private Func, SentryMetric?> _beforeSendMetricSingle;
+ private Func, SentryMetric?> _beforeSendMetricDouble;
+ private Func, SentryMetric?> _beforeSendMetricDecimal;
+
+ internal TraceMetricsCallbacks()
+ {
+ _beforeSendMetricByte = static traceMetric => traceMetric;
+ _beforeSendMetricInt16 = static traceMetric => traceMetric;
+ _beforeSendMetricInt32 = static traceMetric => traceMetric;
+ _beforeSendMetricInt64 = static traceMetric => traceMetric;
+ _beforeSendMetricSingle = static traceMetric => traceMetric;
+ _beforeSendMetricDouble = static traceMetric => traceMetric;
+ _beforeSendMetricDecimal = static traceMetric => traceMetric;
+ }
+
+ //TODO: Integers should be a 64-bit signed integer, while doubles should be a 64-bit floating point number.
+ internal void Set(Func, SentryMetric?> beforeSendMetric) where T : struct
+ {
+ beforeSendMetric ??= static traceMetric => traceMetric;
+
+ if (typeof(T) == typeof(byte))
+ {
+ _beforeSendMetricByte = (Func, SentryMetric?>)(object)beforeSendMetric;
+ }
+ else if (typeof(T) == typeof(int))
+ {
+ _beforeSendMetricInt32 = (Func, SentryMetric?>)(object)beforeSendMetric;
+ }
+ else if (typeof(T) == typeof(float))
+ {
+ _beforeSendMetricSingle = (Func, SentryMetric?>)(object)beforeSendMetric;
+ }
+ else if (typeof(T) == typeof(double))
+ {
+ _beforeSendMetricDouble = (Func, SentryMetric?>)(object)beforeSendMetric;
+ }
+ else if (typeof(T) == typeof(decimal))
+ {
+ _beforeSendMetricDecimal = (Func, SentryMetric?>)(object)beforeSendMetric;
+ }
+ else if (typeof(T) == typeof(short))
+ {
+ _beforeSendMetricInt16 = (Func, SentryMetric?>)(object)beforeSendMetric;
+ }
+ else if (typeof(T) == typeof(long))
+ {
+ _beforeSendMetricInt64 = (Func, SentryMetric?>)(object)beforeSendMetric;
+ }
+ else
+ {
+ SentrySdk.CurrentOptions?._diagnosticLogger?.LogWarning("{0} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, double, and decimal.", typeof(T));
+ }
+ }
+
+ //TODO: Integers should be a 64-bit signed integer, while doubles should be a 64-bit floating point number.
+ internal SentryMetric? Invoke(SentryMetric metric) where T : struct
+ {
+ if (typeof(T) == typeof(byte))
+ {
+ return (SentryMetric?)(object?)_beforeSendMetricByte.Invoke((SentryMetric)(object)metric);
+ }
+ if (typeof(T) == typeof(short))
+ {
+ return (SentryMetric?)(object?)_beforeSendMetricInt16.Invoke((SentryMetric)(object)metric);
+ }
+ if (typeof(T) == typeof(int))
+ {
+ return (SentryMetric?)(object?)_beforeSendMetricInt32.Invoke((SentryMetric)(object)metric);
+ }
+ if (typeof(T) == typeof(long))
+ {
+ return (SentryMetric?)(object?)_beforeSendMetricInt64.Invoke((SentryMetric)(object)metric);
+ }
+ if (typeof(T) == typeof(float))
+ {
+ return (SentryMetric?)(object?)_beforeSendMetricSingle.Invoke((SentryMetric)(object)metric);
+ }
+ if (typeof(T) == typeof(double))
+ {
+ return (SentryMetric?)(object?)_beforeSendMetricDouble.Invoke((SentryMetric)(object)metric);
+ }
+ if (typeof(T) == typeof(decimal))
+ {
+ return (SentryMetric?)(object?)_beforeSendMetricDecimal.Invoke((SentryMetric)(object)metric);
+ }
+
+ System.Diagnostics.Debug.Fail($"Unhandled Metric Type {typeof(T)}.", "This instruction should be unreachable.");
+ return null;
+ }
+ }
+}
diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs
index c769b98370..6db285068a 100644
--- a/src/Sentry/SentryOptions.cs
+++ b/src/Sentry/SentryOptions.cs
@@ -31,7 +31,7 @@ namespace Sentry;
#if __MOBILE__
public partial class SentryOptions
#else
-public class SentryOptions
+public partial class SentryOptions
#endif
{
private Dictionary? _defaultTags;
@@ -1905,4 +1905,51 @@ internal static List GetDefaultInAppExclude() =>
"ServiceStack",
"Java.Interop",
];
+
+ ///
+ /// Sentry features that are currently in an experimental state.
+ ///
+ ///
+ /// Experimental features are subject to binary, source and behavioral breaking changes in future updates.
+ ///
+ public ExperimentalSentryOptions Experimental { get; } = new();
+
+ ///
+ /// Sentry features that are currently in an experimental state.
+ ///
+ ///
+ /// Experimental features are subject to binary, source and behavioral breaking changes in future updates.
+ ///
+ public class ExperimentalSentryOptions
+ {
+ private TraceMetricsCallbacks? _beforeSendMetric;
+
+ internal ExperimentalSentryOptions()
+ {
+ }
+
+ internal TraceMetricsCallbacks? BeforeSendMetricInternal => _beforeSendMetric;
+
+ ///
+ /// When set to , the SDK does not generate and send metrics to Sentry via .
+ /// Defaults to .
+ ///
+ ///
+ public bool EnableMetrics { get; set; } = true;
+
+ ///
+ /// Sets a callback function to be invoked before sending the metric to Sentry.
+ /// When the delegate throws an during invocation, the metric will not be captured.
+ ///
+ ///
+ /// It can be used to modify the metric object before being sent to Sentry.
+ /// To prevent the metric from being sent to Sentry, return .
+ ///
+ ///
+ public void SetBeforeSendMetric(Func, SentryMetric?> beforeSendMetric) where T : struct
+ {
+ _beforeSendMetric ??= new TraceMetricsCallbacks();
+ _beforeSendMetric.Set(beforeSendMetric);
+ }
+ }
}
diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs
index 46da99658e..57b79673ed 100644
--- a/src/Sentry/SentrySdk.cs
+++ b/src/Sentry/SentrySdk.cs
@@ -857,4 +857,30 @@ public static void CauseCrash(CrashType crashType)
[DllImport("libc", EntryPoint = "strlen")]
private static extern IntPtr NativeStrlenLibC(IntPtr strt);
#endif
+
+ ///
+ /// Sentry features that are currently in an experimental state.
+ ///
+ ///
+ /// Experimental features are subject to binary, source and behavioral breaking changes in future updates.
+ ///
+ public static ExperimentalSentrySdk Experimental { get; } = new();
+
+ ///
+ /// Sentry features that are currently in an experimental state.
+ ///
+ ///
+ /// Experimental features are subject to binary, source and behavioral breaking changes in future updates.
+ ///
+ public sealed class ExperimentalSentrySdk
+ {
+ internal ExperimentalSentrySdk()
+ {
+ }
+
+#pragma warning disable SENTRYTRACECONNECTEDMETRICS
+ ///
+ public SentryTraceMetrics Metrics { [DebuggerStepThrough] get => CurrentHub.Metrics; }
+#pragma warning restore SENTRYTRACECONNECTEDMETRICS
+ }
}
diff --git a/src/Sentry/SentryTraceMetrics.Public.cs b/src/Sentry/SentryTraceMetrics.Public.cs
new file mode 100644
index 0000000000..3bf1a75985
--- /dev/null
+++ b/src/Sentry/SentryTraceMetrics.Public.cs
@@ -0,0 +1,124 @@
+namespace Sentry;
+
+public abstract partial class SentryTraceMetrics
+{
+ ///
+ /// Increment a counter.
+ ///
+ /// The name of the metric.
+ /// The value of the metric.
+ /// The scope to capture the metric with.
+ /// The numeric type of the metric.
+ public void AddCounter(string name, T value, Scope? scope = null) where T : struct
+ {
+ CaptureMetric(SentryMetricType.Counter, name, value, null, [], scope);
+ }
+
+ ///
+ /// 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.
+ public void AddCounter(string name, T value, IEnumerable>? attributes = null, Scope? scope = null) where T : struct
+ {
+ CaptureMetric(SentryMetricType.Counter, name, value, null, attributes, scope);
+ }
+
+ ///
+ /// 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.
+ public void AddCounter(string name, T value, ReadOnlySpan> attributes, Scope? scope = null) where T : struct
+ {
+ CaptureMetric(SentryMetricType.Counter, name, value, null, attributes, scope);
+ }
+
+ ///
+ /// Set a gauge value.
+ ///
+ /// The name of the metric.
+ /// The value of the metric.
+ /// The unit of measurement.
+ /// The scope to capture the metric with.
+ /// The numeric type of the metric.
+ public void RecordGauge(string name, T value, string? unit = null, Scope? scope = null) where T : struct
+ {
+ CaptureMetric(SentryMetricType.Gauge, name, value, unit, [], scope);
+ }
+
+ ///
+ /// 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.
+ public void RecordGauge(string name, T value, string? unit = null, IEnumerable>? attributes = null, Scope? scope = null) where T : struct
+ {
+ CaptureMetric(SentryMetricType.Gauge, name, value, unit, attributes, scope);
+ }
+
+ ///
+ /// 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.
+ public void RecordGauge(string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope = null) where T : struct
+ {
+ CaptureMetric(SentryMetricType.Gauge, name, value, unit, attributes, scope);
+ }
+
+ ///
+ /// Add a distribution value.
+ ///
+ /// The name of the metric.
+ /// The value of the metric.
+ /// The unit of measurement.
+ /// The scope to capture the metric with.
+ /// The numeric type of the metric.
+ public void RecordDistribution(string name, T value, string? unit = null, Scope? scope = null) where T : struct
+ {
+ CaptureMetric(SentryMetricType.Distribution, name, value, unit, [], scope);
+ }
+
+ ///
+ /// 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.
+ public void RecordDistribution(string name, T value, string? unit = null, IEnumerable>? attributes = null, Scope? scope = null) where T : struct
+ {
+ CaptureMetric(SentryMetricType.Distribution, name, value, unit, attributes, scope);
+ }
+
+ ///
+ /// 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.
+ public void RecordDistribution(string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope = null) where T : struct
+ {
+ CaptureMetric(SentryMetricType.Distribution, name, value, unit, attributes, scope);
+ }
+}
diff --git a/src/Sentry/SentryTraceMetrics.cs b/src/Sentry/SentryTraceMetrics.cs
new file mode 100644
index 0000000000..9c0be19f8e
--- /dev/null
+++ b/src/Sentry/SentryTraceMetrics.cs
@@ -0,0 +1,63 @@
+using Sentry.Infrastructure;
+using Sentry.Internal;
+
+namespace Sentry;
+
+///
+/// Creates and sends metrics to Sentry.
+///
+public abstract partial class SentryTraceMetrics
+{
+ internal static SentryTraceMetrics Create(IHub hub, SentryOptions options, ISystemClock clock)
+ => Create(hub, options, clock, 100, TimeSpan.FromSeconds(5));
+
+ internal static SentryTraceMetrics Create(IHub hub, SentryOptions options, ISystemClock clock, int batchCount, TimeSpan batchInterval)
+ {
+ return options.Experimental.EnableMetrics
+ ? new DefaultSentryTraceMetrics(hub, options, clock, batchCount, batchInterval)
+ : DisabledSentryTraceMetrics.Instance;
+ }
+
+ private protected SentryTraceMetrics()
+ {
+ }
+
+ ///
+ /// 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, IEnumerable>? attributes, Scope? scope) where T : struct;
+
+ ///
+ /// 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, ReadOnlySpan> attributes, Scope? scope) where T : struct;
+
+ ///
+ /// Buffers a Sentry Metric item
+ /// via the associated Batch Processor.
+ ///
+ /// The metric.
+ /// The numeric type of the metric.
+ protected internal abstract void CaptureMetric(SentryMetric metric) where T : struct;
+
+ ///
+ /// Clears all buffers for this metrics and causes any buffered metrics to be sent by the underlying .
+ ///
+ protected internal abstract void Flush();
+}
diff --git a/test/Sentry.Testing/BindableTests.cs b/test/Sentry.Testing/BindableTests.cs
index 68dd553a36..69968238c8 100644
--- a/test/Sentry.Testing/BindableTests.cs
+++ b/test/Sentry.Testing/BindableTests.cs
@@ -32,6 +32,7 @@ private static IEnumerable GetBindableProperties(IEnumerable Entries { get; } = new();
+ internal List Metrics { get; } = new();
+
+ ///
+ private protected override void CaptureMetric(SentryMetricType type, string name, T value, string? unit, IEnumerable>? attributes, Scope? scope) where T : struct
+ {
+ Entries.Add(MetricEntry.Create(type, name, value, unit, attributes, scope));
+ }
+
+ ///
+ private protected override void CaptureMetric(SentryMetricType type, string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope) where T : struct
+ {
+ Entries.Add(MetricEntry.Create(type, name, value, unit, attributes.ToArray(), scope));
+ }
+
+ ///
+ protected internal override void CaptureMetric(SentryMetric metric) where T : struct
+ {
+ Metrics.Add(metric);
+ }
+
+ ///
+ protected internal override void Flush()
+ {
+ // no-op
+ }
+
+ public sealed class MetricEntry : IEquatable
+ {
+ public static MetricEntry Create(SentryMetricType type, string name, object value, string? unit, IEnumerable>? attributes, Scope? scope)
+ {
+ return new MetricEntry(type, name, value, unit, attributes is null ? ImmutableDictionary.Empty : attributes.ToImmutableDictionary(), scope);
+ }
+
+ private MetricEntry(SentryMetricType type, string name, object value, string? unit, ImmutableDictionary attributes, Scope? scope)
+ {
+ Type = type;
+ Name = name;
+ Value = value;
+ Unit = unit;
+ Attributes = attributes;
+ Scope = scope;
+ }
+
+ public SentryMetricType Type { get; }
+ public string Name { get; }
+ public object Value { get; }
+ public string? Unit { get; }
+ public ImmutableDictionary Attributes { get; }
+ public Scope? Scope { get; }
+
+ public void AssertEqual(SentryMetricType type, string name, object value, string? unit, IEnumerable>? attributes, Scope? scope)
+ {
+ var expected = Create(type, name, value, unit, attributes, scope);
+ Assert.Equal(expected, this);
+ }
+
+ public bool Equals(MetricEntry? other)
+ {
+ if (other is null)
+ {
+ return false;
+ }
+
+ if (ReferenceEquals(this, other))
+ {
+ return true;
+ }
+
+ return Type == other.Type
+ && string.Equals(Name, other.Name, StringComparison.Ordinal)
+ && Value.Equals(other.Value)
+ && string.Equals(Unit, other.Unit, StringComparison.Ordinal)
+ && Attributes.SequenceEqual(other.Attributes, AttributeEqualityComparer.Instance)
+ && ReferenceEquals(Scope, other.Scope);
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is MetricEntry other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ throw new UnreachableException();
+ }
+ }
+
+ private sealed class AttributeEqualityComparer : IEqualityComparer>
+ {
+ public static AttributeEqualityComparer Instance { get; } = new AttributeEqualityComparer();
+
+ private AttributeEqualityComparer()
+ {
+ }
+
+ public bool Equals(KeyValuePair x, KeyValuePair y)
+ {
+ return string.Equals(x.Key, y.Key, StringComparison.Ordinal)
+ && Equals(x.Value, y.Value);
+ }
+
+ public int GetHashCode(KeyValuePair obj)
+ {
+ return HashCode.Combine(obj.Key, obj.Value);
+ }
+ }
+}
diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt
index 458d39be99..123881cb39 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt
@@ -197,6 +197,8 @@ namespace Sentry
bool IsSessionActive { get; }
Sentry.SentryId LastEventId { get; }
Sentry.SentryStructuredLogger Logger { get; }
+ [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")]
+ Sentry.SentryTraceMetrics Metrics { get; }
void BindException(System.Exception exception, Sentry.ISpan span);
Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope);
Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope);
@@ -664,6 +666,32 @@ namespace Sentry
protected override System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { }
protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { }
}
+ public enum SentryMetricType
+ {
+ Counter = 0,
+ Gauge = 1,
+ Distribution = 2,
+ }
+ [System.Diagnostics.DebuggerDisplay("SentryMetric \\{ Type = {Type}, Name = \'{Name}\', Value = {Value} \\}")]
+ [System.Runtime.CompilerServices.RequiredMember]
+ public sealed class SentryMetric
+ where T : struct
+ {
+ public System.Collections.Generic.IReadOnlyDictionary Attributes { get; }
+ [System.Runtime.CompilerServices.RequiredMember]
+ public string Name { get; init; }
+ public Sentry.SpanId? SpanId { get; init; }
+ [System.Runtime.CompilerServices.RequiredMember]
+ public System.DateTimeOffset Timestamp { get; init; }
+ [System.Runtime.CompilerServices.RequiredMember]
+ public Sentry.SentryId TraceId { get; init; }
+ [System.Runtime.CompilerServices.RequiredMember]
+ public Sentry.SentryMetricType Type { get; init; }
+ public string? Unit { get; init; }
+ [System.Runtime.CompilerServices.RequiredMember]
+ public T Value { get; init; }
+ public void SetAttribute(string key, object value) { }
+ }
public enum SentryMonitorInterval
{
Year = 0,
@@ -715,6 +743,7 @@ namespace Sentry
public bool EnableScopeSync { get; set; }
public bool EnableSpotlight { get; set; }
public string? Environment { get; set; }
+ public Sentry.SentryOptions.ExperimentalSentryOptions Experimental { get; }
public System.Collections.Generic.IList FailedRequestStatusCodes { get; set; }
public System.Collections.Generic.IList FailedRequestTargets { get; set; }
public System.TimeSpan FlushTimeout { get; set; }
@@ -802,6 +831,12 @@ namespace Sentry
public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { }
public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { }
public Sentry.SentryOptions UseStackTraceFactory(Sentry.Extensibility.ISentryStackTraceFactory sentryStackTraceFactory) { }
+ public class ExperimentalSentryOptions
+ {
+ public bool EnableMetrics { get; set; }
+ public void SetBeforeSendMetric(System.Func, Sentry.SentryMetric?> beforeSendMetric)
+ where T : struct { }
+ }
}
public sealed class SentryPackage : Sentry.ISentryJsonSerializable
{
@@ -831,6 +866,7 @@ namespace Sentry
}
public static class SentrySdk
{
+ public static Sentry.SentrySdk.ExperimentalSentrySdk Experimental { get; }
public static bool IsEnabled { get; }
public static bool IsSessionActive { get; }
public static Sentry.SentryId LastEventId { get; }
@@ -894,6 +930,10 @@ namespace Sentry
public static Sentry.ITransactionTracer StartTransaction(string name, string operation, Sentry.SentryTraceHeader traceHeader) { }
public static Sentry.ITransactionTracer StartTransaction(string name, string operation, string? description) { }
public static void UnsetTag(string key) { }
+ public sealed class ExperimentalSentrySdk
+ {
+ public Sentry.SentryTraceMetrics Metrics { get; }
+ }
}
public class SentrySession : Sentry.ISentrySession
{
@@ -1011,6 +1051,30 @@ namespace Sentry
public override string ToString() { }
public static Sentry.SentryTraceHeader? Parse(string value) { }
}
+ public abstract class SentryTraceMetrics
+ {
+ public void AddCounter(string name, T value, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void AddCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void AddCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null)
+ where T : struct { }
+ protected abstract void CaptureMetric(Sentry.SentryMetric metric)
+ where T : struct;
+ protected abstract void Flush();
+ public void RecordDistribution(string name, T value, string? unit = null, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void RecordDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void RecordDistribution(string name, T value, string? unit = null, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void RecordGauge(string name, T value, string? unit = null, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void RecordGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void RecordGauge(string name, T value, string? unit = null, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null)
+ where T : struct { }
+ }
public class SentryTransaction : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISentryJsonSerializable, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext
{
public SentryTransaction(Sentry.ITransactionTracer tracer) { }
@@ -1385,6 +1449,8 @@ namespace Sentry.Extensibility
public bool IsSessionActive { get; }
public Sentry.SentryId LastEventId { get; }
public Sentry.SentryStructuredLogger Logger { get; }
+ [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")]
+ public Sentry.SentryTraceMetrics Metrics { get; }
public void BindClient(Sentry.ISentryClient client) { }
public void BindException(System.Exception exception, Sentry.ISpan span) { }
public Sentry.SentryId CaptureCheckIn(string monitorSlug, Sentry.CheckInStatus status, Sentry.SentryId? sentryId = default, System.TimeSpan? duration = default, Sentry.Scope? scope = null, System.Action? configureMonitorOptions = null) { }
@@ -1432,6 +1498,8 @@ namespace Sentry.Extensibility
public bool IsSessionActive { get; }
public Sentry.SentryId LastEventId { get; }
public Sentry.SentryStructuredLogger Logger { get; }
+ [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")]
+ public Sentry.SentryTraceMetrics 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 458d39be99..123881cb39 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
@@ -197,6 +197,8 @@ namespace Sentry
bool IsSessionActive { get; }
Sentry.SentryId LastEventId { get; }
Sentry.SentryStructuredLogger Logger { get; }
+ [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")]
+ Sentry.SentryTraceMetrics Metrics { get; }
void BindException(System.Exception exception, Sentry.ISpan span);
Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope);
Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope);
@@ -664,6 +666,32 @@ namespace Sentry
protected override System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { }
protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { }
}
+ public enum SentryMetricType
+ {
+ Counter = 0,
+ Gauge = 1,
+ Distribution = 2,
+ }
+ [System.Diagnostics.DebuggerDisplay("SentryMetric \\{ Type = {Type}, Name = \'{Name}\', Value = {Value} \\}")]
+ [System.Runtime.CompilerServices.RequiredMember]
+ public sealed class SentryMetric
+ where T : struct
+ {
+ public System.Collections.Generic.IReadOnlyDictionary Attributes { get; }
+ [System.Runtime.CompilerServices.RequiredMember]
+ public string Name { get; init; }
+ public Sentry.SpanId? SpanId { get; init; }
+ [System.Runtime.CompilerServices.RequiredMember]
+ public System.DateTimeOffset Timestamp { get; init; }
+ [System.Runtime.CompilerServices.RequiredMember]
+ public Sentry.SentryId TraceId { get; init; }
+ [System.Runtime.CompilerServices.RequiredMember]
+ public Sentry.SentryMetricType Type { get; init; }
+ public string? Unit { get; init; }
+ [System.Runtime.CompilerServices.RequiredMember]
+ public T Value { get; init; }
+ public void SetAttribute(string key, object value) { }
+ }
public enum SentryMonitorInterval
{
Year = 0,
@@ -715,6 +743,7 @@ namespace Sentry
public bool EnableScopeSync { get; set; }
public bool EnableSpotlight { get; set; }
public string? Environment { get; set; }
+ public Sentry.SentryOptions.ExperimentalSentryOptions Experimental { get; }
public System.Collections.Generic.IList FailedRequestStatusCodes { get; set; }
public System.Collections.Generic.IList FailedRequestTargets { get; set; }
public System.TimeSpan FlushTimeout { get; set; }
@@ -802,6 +831,12 @@ namespace Sentry
public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { }
public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { }
public Sentry.SentryOptions UseStackTraceFactory(Sentry.Extensibility.ISentryStackTraceFactory sentryStackTraceFactory) { }
+ public class ExperimentalSentryOptions
+ {
+ public bool EnableMetrics { get; set; }
+ public void SetBeforeSendMetric(System.Func, Sentry.SentryMetric?> beforeSendMetric)
+ where T : struct { }
+ }
}
public sealed class SentryPackage : Sentry.ISentryJsonSerializable
{
@@ -831,6 +866,7 @@ namespace Sentry
}
public static class SentrySdk
{
+ public static Sentry.SentrySdk.ExperimentalSentrySdk Experimental { get; }
public static bool IsEnabled { get; }
public static bool IsSessionActive { get; }
public static Sentry.SentryId LastEventId { get; }
@@ -894,6 +930,10 @@ namespace Sentry
public static Sentry.ITransactionTracer StartTransaction(string name, string operation, Sentry.SentryTraceHeader traceHeader) { }
public static Sentry.ITransactionTracer StartTransaction(string name, string operation, string? description) { }
public static void UnsetTag(string key) { }
+ public sealed class ExperimentalSentrySdk
+ {
+ public Sentry.SentryTraceMetrics Metrics { get; }
+ }
}
public class SentrySession : Sentry.ISentrySession
{
@@ -1011,6 +1051,30 @@ namespace Sentry
public override string ToString() { }
public static Sentry.SentryTraceHeader? Parse(string value) { }
}
+ public abstract class SentryTraceMetrics
+ {
+ public void AddCounter(string name, T value, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void AddCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void AddCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null)
+ where T : struct { }
+ protected abstract void CaptureMetric(Sentry.SentryMetric metric)
+ where T : struct;
+ protected abstract void Flush();
+ public void RecordDistribution(string name, T value, string? unit = null, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void RecordDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void RecordDistribution(string name, T value, string? unit = null, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void RecordGauge(string name, T value, string? unit = null, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void RecordGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void RecordGauge(string name, T value, string? unit = null, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null)
+ where T : struct { }
+ }
public class SentryTransaction : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISentryJsonSerializable, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext
{
public SentryTransaction(Sentry.ITransactionTracer tracer) { }
@@ -1385,6 +1449,8 @@ namespace Sentry.Extensibility
public bool IsSessionActive { get; }
public Sentry.SentryId LastEventId { get; }
public Sentry.SentryStructuredLogger Logger { get; }
+ [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")]
+ public Sentry.SentryTraceMetrics Metrics { get; }
public void BindClient(Sentry.ISentryClient client) { }
public void BindException(System.Exception exception, Sentry.ISpan span) { }
public Sentry.SentryId CaptureCheckIn(string monitorSlug, Sentry.CheckInStatus status, Sentry.SentryId? sentryId = default, System.TimeSpan? duration = default, Sentry.Scope? scope = null, System.Action? configureMonitorOptions = null) { }
@@ -1432,6 +1498,8 @@ namespace Sentry.Extensibility
public bool IsSessionActive { get; }
public Sentry.SentryId LastEventId { get; }
public Sentry.SentryStructuredLogger Logger { get; }
+ [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")]
+ public Sentry.SentryTraceMetrics 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.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
index 458d39be99..123881cb39 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
@@ -197,6 +197,8 @@ namespace Sentry
bool IsSessionActive { get; }
Sentry.SentryId LastEventId { get; }
Sentry.SentryStructuredLogger Logger { get; }
+ [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS")]
+ Sentry.SentryTraceMetrics Metrics { get; }
void BindException(System.Exception exception, Sentry.ISpan span);
Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope);
Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.SentryHint? hint, System.Action configureScope);
@@ -664,6 +666,32 @@ namespace Sentry
protected override System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { }
protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { }
}
+ public enum SentryMetricType
+ {
+ Counter = 0,
+ Gauge = 1,
+ Distribution = 2,
+ }
+ [System.Diagnostics.DebuggerDisplay("SentryMetric \\{ Type = {Type}, Name = \'{Name}\', Value = {Value} \\}")]
+ [System.Runtime.CompilerServices.RequiredMember]
+ public sealed class SentryMetric
+ where T : struct
+ {
+ public System.Collections.Generic.IReadOnlyDictionary Attributes { get; }
+ [System.Runtime.CompilerServices.RequiredMember]
+ public string Name { get; init; }
+ public Sentry.SpanId? SpanId { get; init; }
+ [System.Runtime.CompilerServices.RequiredMember]
+ public System.DateTimeOffset Timestamp { get; init; }
+ [System.Runtime.CompilerServices.RequiredMember]
+ public Sentry.SentryId TraceId { get; init; }
+ [System.Runtime.CompilerServices.RequiredMember]
+ public Sentry.SentryMetricType Type { get; init; }
+ public string? Unit { get; init; }
+ [System.Runtime.CompilerServices.RequiredMember]
+ public T Value { get; init; }
+ public void SetAttribute(string key, object value) { }
+ }
public enum SentryMonitorInterval
{
Year = 0,
@@ -715,6 +743,7 @@ namespace Sentry
public bool EnableScopeSync { get; set; }
public bool EnableSpotlight { get; set; }
public string? Environment { get; set; }
+ public Sentry.SentryOptions.ExperimentalSentryOptions Experimental { get; }
public System.Collections.Generic.IList FailedRequestStatusCodes { get; set; }
public System.Collections.Generic.IList FailedRequestTargets { get; set; }
public System.TimeSpan FlushTimeout { get; set; }
@@ -802,6 +831,12 @@ namespace Sentry
public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { }
public void SetBeforeSendTransaction(System.Func beforeSendTransaction) { }
public Sentry.SentryOptions UseStackTraceFactory(Sentry.Extensibility.ISentryStackTraceFactory sentryStackTraceFactory) { }
+ public class ExperimentalSentryOptions
+ {
+ public bool EnableMetrics { get; set; }
+ public void SetBeforeSendMetric(System.Func, Sentry.SentryMetric?> beforeSendMetric)
+ where T : struct { }
+ }
}
public sealed class SentryPackage : Sentry.ISentryJsonSerializable
{
@@ -831,6 +866,7 @@ namespace Sentry
}
public static class SentrySdk
{
+ public static Sentry.SentrySdk.ExperimentalSentrySdk Experimental { get; }
public static bool IsEnabled { get; }
public static bool IsSessionActive { get; }
public static Sentry.SentryId LastEventId { get; }
@@ -894,6 +930,10 @@ namespace Sentry
public static Sentry.ITransactionTracer StartTransaction(string name, string operation, Sentry.SentryTraceHeader traceHeader) { }
public static Sentry.ITransactionTracer StartTransaction(string name, string operation, string? description) { }
public static void UnsetTag(string key) { }
+ public sealed class ExperimentalSentrySdk
+ {
+ public Sentry.SentryTraceMetrics Metrics { get; }
+ }
}
public class SentrySession : Sentry.ISentrySession
{
@@ -1011,6 +1051,30 @@ namespace Sentry
public override string ToString() { }
public static Sentry.SentryTraceHeader? Parse(string value) { }
}
+ public abstract class SentryTraceMetrics
+ {
+ public void AddCounter(string name, T value, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void AddCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void AddCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null)
+ where T : struct { }
+ protected abstract void CaptureMetric(Sentry.SentryMetric metric)
+ where T : struct;
+ protected abstract void Flush();
+ public void RecordDistribution(string name, T value, string? unit = null, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void RecordDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void RecordDistribution(string name, T value, string? unit = null, System.Collections.Generic.IEnumerable>? attributes = null, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void RecordGauge(string name, T value, string? unit = null, Sentry.Scope? scope = null)
+ where T : struct { }
+ public void RecordGauge(string name, T value, string? unit, System.ReadOnlySpan