diff --git a/CHANGELOG.md b/CHANGELOG.md index 26bbf20371..a82b5f3071 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,28 @@ ## Unreleased +### BREAKING CHANGES + +- Rename [Trace-connected Metrics](https://docs.sentry.io/product/explore/metrics/) APIs to avoid implying aggregation ([#4834](https://github.com/getsentry/sentry-dotnet/pull/4834)) + - from `AddCounter` to `EmitCounter` + - from `RecordDistribution` to `EmitDistribution` + - from `RecordGauge` to `EmitGauge` + +### Features + +- Validate [Trace-connected Metrics](https://docs.sentry.io/product/explore/metrics/) ([#4834](https://github.com/getsentry/sentry-dotnet/pull/4834)) + +### Fixes + +- Attributes for [Trace-connected Metrics](https://docs.sentry.io/product/explore/metrics/) set via `SetBeforeSendLog` callback ([#4834](https://github.com/getsentry/sentry-dotnet/pull/4834)) +- Disallow unsupported 128-bit floating point numbers (i.e. `decimal`) for [Trace-connected Metrics](https://docs.sentry.io/product/explore/metrics/) ([#4834](https://github.com/getsentry/sentry-dotnet/pull/4834)) + +## 6.1.0-alpha.1 + ### Features - Extended `SentryThread` by `Main` to allow indication whether the thread is considered the current main thread ([#4807](https://github.com/getsentry/sentry-dotnet/pull/4807)) +- Add _experimental_ support for [Sentry trace-connected Metrics](https://docs.sentry.io/product/explore/metrics/) ([#4834](https://github.com/getsentry/sentry-dotnet/pull/4834)) ### Dependencies diff --git a/Directory.Build.props b/Directory.Build.props index 0c8421134f..7e15f5723e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 6.0.0 + 6.1.0-alpha.1 13 true true @@ -13,6 +13,7 @@ $(NoWarn);SENTRY0001 + $(NoWarn);SENTRYTRACECONNECTEDMETRICS $(NoWarn);CS8002 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.Compiler.Extensions/AnalyzerReleases.Shipped.md b/src/Sentry.Compiler.Extensions/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000000..60b59dd99b --- /dev/null +++ b/src/Sentry.Compiler.Extensions/AnalyzerReleases.Shipped.md @@ -0,0 +1,3 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + diff --git a/src/Sentry.Compiler.Extensions/AnalyzerReleases.Unshipped.md b/src/Sentry.Compiler.Extensions/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000000..f8fc0f9c86 --- /dev/null +++ b/src/Sentry.Compiler.Extensions/AnalyzerReleases.Unshipped.md @@ -0,0 +1,8 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +SENTRY1001 | Support | Warning | TraceConnectedMetricsAnalyzer \ No newline at end of file diff --git a/src/Sentry.Compiler.Extensions/Analyzers/TraceConnectedMetricsAnalyzer.cs b/src/Sentry.Compiler.Extensions/Analyzers/TraceConnectedMetricsAnalyzer.cs new file mode 100644 index 0000000000..ff8dd50f95 --- /dev/null +++ b/src/Sentry.Compiler.Extensions/Analyzers/TraceConnectedMetricsAnalyzer.cs @@ -0,0 +1,114 @@ +using System; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Sentry.Compiler.Extensions.Analyzers; + +/// +/// Guide consumers to use the public API of Sentry Trace-connected Metrics correctly. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class TraceConnectedMetricsAnalyzer : DiagnosticAnalyzer +{ + private static readonly string Title = "Unsupported numeric type of Metric"; + private static readonly string MessageFormat = "{0} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, and double."; + private static readonly string Description = "Integers should be a 64-bit signed integer, while doubles should be a 64-bit floating point number."; + + private static readonly DiagnosticDescriptor Rule = new( + id: DiagnosticIds.Sentry1001, + title: Title, + messageFormat: MessageFormat, + category: DiagnosticCategories.Support, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: Description, + helpLinkUri: null + ); + + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(Rule); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterOperationAction(Execute, OperationKind.Invocation); + } + + private static void Execute(OperationAnalysisContext context) + { + Debug.Assert(context.Operation.Language == LanguageNames.CSharp); + Debug.Assert(context.Operation.Kind is OperationKind.Invocation); + + context.CancellationToken.ThrowIfCancellationRequested(); + + if (context.Operation is not IInvocationOperation invocation) + { + return; + } + + var method = invocation.TargetMethod; + if (method.DeclaredAccessibility != Accessibility.Public || method.IsAbstract || method.IsVirtual || method.IsStatic || !method.ReturnsVoid || method.Parameters.Length == 0) + { + return; + } + + if (!method.IsGenericMethod || method.Arity != 1 || method.TypeArguments.Length != 1) + { + return; + } + + if (method.ContainingAssembly is null || method.ContainingAssembly.Name != "Sentry") + { + return; + } + + if (method.ContainingNamespace is null || method.ContainingNamespace.Name != "Sentry") + { + return; + } + + string fullyQualifiedMetadataName; + if (method.Name is "EmitCounter" or "EmitGauge" or "EmitDistribution") + { + fullyQualifiedMetadataName = "Sentry.SentryTraceMetrics"; + } + else if (method.Name is "SetBeforeSendMetric") + { + fullyQualifiedMetadataName = "Sentry.SentryOptions+ExperimentalSentryOptions"; + } + else + { + return; + } + + var typeArgument = method.TypeArguments[0]; + if (typeArgument.SpecialType is SpecialType.System_Byte or SpecialType.System_Int16 or SpecialType.System_Int32 or SpecialType.System_Int64 or SpecialType.System_Single or SpecialType.System_Double) + { + return; + } + + if (typeArgument is ITypeParameterSymbol) + { + return; + } + + var sentryType = context.Compilation.GetTypeByMetadataName(fullyQualifiedMetadataName); + if (sentryType is null) + { + return; + } + + if (!SymbolEqualityComparer.Default.Equals(method.ContainingType, sentryType)) + { + return; + } + + var location = invocation.Syntax.GetLocation(); + var diagnostic = Diagnostic.Create(Rule, location, typeArgument.ToDisplayString(SymbolDisplayFormats.FullNameFormat)); + context.ReportDiagnostic(diagnostic); + } +} diff --git a/src/Sentry.Compiler.Extensions/DiagnosticCategories.cs b/src/Sentry.Compiler.Extensions/DiagnosticCategories.cs new file mode 100644 index 0000000000..3fe408be60 --- /dev/null +++ b/src/Sentry.Compiler.Extensions/DiagnosticCategories.cs @@ -0,0 +1,6 @@ +namespace Sentry.Compiler.Extensions; + +internal static class DiagnosticCategories +{ + internal const string Support = nameof(Support); +} diff --git a/src/Sentry.Compiler.Extensions/DiagnosticIds.cs b/src/Sentry.Compiler.Extensions/DiagnosticIds.cs new file mode 100644 index 0000000000..fa2f8a3d94 --- /dev/null +++ b/src/Sentry.Compiler.Extensions/DiagnosticIds.cs @@ -0,0 +1,6 @@ +namespace Sentry.Compiler.Extensions; + +internal static class DiagnosticIds +{ + internal const string Sentry1001 = "SENTRY1001"; +} diff --git a/src/Sentry.Compiler.Extensions/Sentry.Compiler.Extensions.csproj b/src/Sentry.Compiler.Extensions/Sentry.Compiler.Extensions.csproj index 9fb31748a3..8e5a6c88aa 100644 --- a/src/Sentry.Compiler.Extensions/Sentry.Compiler.Extensions.csproj +++ b/src/Sentry.Compiler.Extensions/Sentry.Compiler.Extensions.csproj @@ -15,11 +15,25 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + + + + + + + + + diff --git a/src/Sentry.Compiler.Extensions/SymbolDisplayFormats.cs b/src/Sentry.Compiler.Extensions/SymbolDisplayFormats.cs new file mode 100644 index 0000000000..85af2df64b --- /dev/null +++ b/src/Sentry.Compiler.Extensions/SymbolDisplayFormats.cs @@ -0,0 +1,12 @@ +using Microsoft.CodeAnalysis; + +namespace Sentry.Compiler.Extensions; + +internal static class SymbolDisplayFormats +{ + internal static SymbolDisplayFormat FullNameFormat { get; } = new SymbolDisplayFormat( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters + ); +} 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..6c6a7ae31d 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", UrlFormat = "https://github.com/getsentry/sentry-dotnet/discussions/4838")] + public SentryTraceMetrics Metrics => DisabledSentryTraceMetrics.Instance; } diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs index 45499369e6..6ec4c79104 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", UrlFormat = "https://github.com/getsentry/sentry-dotnet/discussions/4838")] + 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..2d48844a9e 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", UrlFormat = "https://github.com/getsentry/sentry-dotnet/discussions/4838")] + 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..bb37bc474e 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 where TItem : notnull { 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}", item.GetType().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..df02cfa44f --- /dev/null +++ b/src/Sentry/Internal/DefaultSentryTraceMetrics.cs @@ -0,0 +1,103 @@ +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 + { + 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, attributes, scope); + CaptureMetric(metric); + } + + /// + private protected override void CaptureMetric(SentryMetricType type, string name, T value, string? unit, ReadOnlySpan> 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, attributes, scope); + CaptureMetric(metric); + } + + /// + protected internal override void CaptureMetric(SentryMetric metric) where T : struct + { + Debug.Assert(SentryMetric.IsSupported(typeof(T))); + Debug.Assert(!string.IsNullOrEmpty(metric.Name)); + + 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..290d941b1f --- /dev/null +++ b/src/Sentry/SentryMetric.Factory.cs @@ -0,0 +1,93 @@ +using Sentry.Infrastructure; +using Sentry.Internal; + +namespace Sentry; + +internal static class SentryMetric +{ + internal static SentryMetric Create(IHub hub, SentryOptions options, ISystemClock clock, SentryMetricType type, string name, T value, string? unit, IEnumerable>? attributes, Scope? scope) where T : struct + { + Debug.Assert(IsSupported()); + Debug.Assert(!string.IsNullOrEmpty(name)); + 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(); + 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.SetDefaultAttributes(options, scope?.Sdk ?? SdkVersion.Instance); + metric.Apply(scope); + + metric.SetAttributes(attributes); + + return metric; + } + + internal static SentryMetric Create(IHub hub, SentryOptions options, ISystemClock clock, SentryMetricType type, string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope) where T : struct + { + Debug.Assert(IsSupported()); + Debug.Assert(!string.IsNullOrEmpty(name)); + 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(); + 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.SetDefaultAttributes(options, scope?.Sdk ?? SdkVersion.Instance); + metric.Apply(scope); + + metric.SetAttributes(attributes); + + return metric; + } + + internal static void GetTraceIdAndSpanId(IHub hub, out SentryId traceId, out SpanId? spanId) + { + var activeSpan = hub.GetSpan(); + if (activeSpan is not null) + { + traceId = activeSpan.TraceId; + spanId = activeSpan.SpanId; + return; + } + + // set "span_id" to the ID of the Span that was active when the Metric was emitted + // do not set "span_id" if there was no active Span + spanId = null; + + var scope = hub.GetScope(); + if (scope is not null) + { + traceId = scope.PropagationContext.TraceId; + return; + } + + Debug.Assert(hub is not Hub, "In case of a 'full' Hub, there is always a Scope. Otherwise (disabled) there is no Scope, but this branch should be unreachable."); + traceId = SentryId.Empty; + } + + private static bool IsSupported() where T : struct + { + var valueType = typeof(T); + return IsSupported(valueType); + } + + internal static bool IsSupported(Type valueType) + { + return valueType == typeof(long) || valueType == typeof(double) + || valueType == typeof(int) || valueType == typeof(float) + || valueType == typeof(short) || valueType == typeof(byte); + } +} 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..ba62ca2d7d --- /dev/null +++ b/src/Sentry/SentryMetric.cs @@ -0,0 +1,376 @@ +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 readonly 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; + // 7 is the number of built-in attributes, so we start with that. + _attributes = new Dictionary(7); + } + + /// + /// 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. + /// + 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. + /// + public SpanId? SpanId { get; init; } + + /// + /// The unit of measurement for the metric value. + /// + /// + /// Only used for and . + /// + public string? Unit { get; init; } + + /// + /// Gets the attribute value associated with the specified key. + /// + /// + /// Returns if the contains an attribute with the specified key which is of type and it's value is not . + /// Otherwise . + /// Supported types: + /// + /// + /// Type + /// Range + /// + /// + /// string + /// and + /// + /// + /// boolean + /// and + /// + /// + /// integer + /// 64-bit signed integral numeric types + /// + /// + /// double + /// 64-bit floating-point numeric types + /// + /// + /// Unsupported types: + /// + /// + /// Type + /// Result + /// + /// + /// + /// ToString as "type": "string" + /// + /// + /// Collections + /// ToString as "type": "string" + /// + /// + /// + /// ignored + /// + /// + /// + /// + public bool TryGetAttribute(string key, [MaybeNullWhen(false)] out TAttribute value) + { + if (_attributes.TryGetValue(key, out var attribute) && attribute.Value is TAttribute attributeValue) + { + value = attributeValue; + return true; + } + + value = default; + return false; + } + + /// + /// Set a key-value pair of data attached to the metric. + /// + public void SetAttribute(string key, TAttribute value) where TAttribute : notnull + { + if (value is null) + { + return; + } + + _attributes[key] = new SentryAttribute(value); + } + + internal void SetAttribute(string key, string value) + { + _attributes[key] = new SentryAttribute(value, "string"); + } + + internal void SetAttribute(string key, char value) + { + _attributes[key] = new SentryAttribute(value.ToString(), "string"); + } + + internal void SetAttribute(string key, int value) + { + _attributes[key] = new SentryAttribute(value, "integer"); + } + + internal void SetDefaultAttributes(SentryOptions options, SdkVersion sdk) + { + var environment = options.SettingLocator.GetEnvironment(); + SetAttribute("sentry.environment", environment); + + var release = options.SettingLocator.GetRelease(); + if (release is not null) + { + SetAttribute("sentry.release", release); + } + + if (sdk.Name is { } name) + { + SetAttribute("sentry.sdk.name", name); + } + if (sdk.Version is { } version) + { + SetAttribute("sentry.sdk.version", version); + } + } + + internal void SetAttributes(IEnumerable>? attributes) + { + if (attributes is null) + { + return; + } + +#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] = new SentryAttribute(attribute.Value); + } + } + + internal void SetAttributes(ReadOnlySpan> attributes) + { + if (attributes.IsEmpty) + { + return; + } + +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + _ = _attributes.EnsureCapacity(_attributes.Count + attributes.Length); +#endif + + foreach (var attribute in attributes) + { + _attributes[attribute.Key] = new SentryAttribute(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); + } + + writer.WritePropertyName("attributes"); + writer.WriteStartObject(); + + foreach (var attribute in _attributes) + { + SentryAttributeSerializer.WriteAttribute(writer, attribute.Key, attribute.Value, logger); + } + + writer.WriteEndObject(); + + writer.WriteEndObject(); + } +} + +// 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 +{ + internal static void WriteMetricValue(this Utf8JsonWriter writer, string propertyName, T value) where T : struct + { + var type = typeof(T); + + 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(int)) + { + writer.WriteNumber(propertyName, (int)(object)value); + } + else if (type == typeof(float)) + { + writer.WriteNumber(propertyName, (float)(object)value); + } + else if (type == typeof(short)) + { + writer.WriteNumber(propertyName, (short)(object)value); + } + else if (type == typeof(byte)) + { + writer.WriteNumber(propertyName, (byte)(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.cs b/src/Sentry/SentryOptions.cs index c769b98370..3f6bc689e4 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -1370,6 +1370,8 @@ public SentryOptions() ); NetworkStatusListener = new PollingNetworkStatusListener(this); + + Experimental = new ExperimentalSentryOptions(this); } /// @@ -1905,4 +1907,55 @@ 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; } + + /// + /// 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 readonly SentryOptions _options; + private SentryTraceMetricsCallbacks? _beforeSendMetric; + + internal ExperimentalSentryOptions(SentryOptions options) + { + _options = options; + } + + internal SentryTraceMetricsCallbacks? 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 of numeric type to Sentry. + /// When the delegate throws an during invocation, the metric will not be captured. + /// + /// The numeric type of the metric. + /// + /// It can be used to modify the metric object before being sent to Sentry. + /// To prevent the metric from being sent to Sentry, return . + /// Supported numeric value types for are , , , , , and . + /// + /// + public void SetBeforeSendMetric(Func, SentryMetric?> beforeSendMetric) where T : struct + { + _beforeSendMetric ??= new SentryTraceMetricsCallbacks(); + _beforeSendMetric.Set(beforeSendMetric, _options.DiagnosticLogger); + } + } } 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.Counter.cs b/src/Sentry/SentryTraceMetrics.Counter.cs new file mode 100644 index 0000000000..60f87f2d78 --- /dev/null +++ b/src/Sentry/SentryTraceMetrics.Counter.cs @@ -0,0 +1,57 @@ +namespace Sentry; + +public abstract partial class SentryTraceMetrics +{ + /// + /// Increment a counter. + /// + /// The name of the metric. + /// The value of the metric. + /// The numeric type of the metric. + /// Supported numeric value types for are , , , , , and . + public void EmitCounter(string name, T value) where T : struct + { + CaptureMetric(SentryMetricType.Counter, name, value, null, [], null); + } + + /// + /// 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. + /// Supported numeric value types for are , , , , , and . + public void EmitCounter(string name, T value, Scope? scope) 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. + /// Supported numeric value types for are , , , , , and . + public void EmitCounter(string name, T value, IEnumerable>? attributes, 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. + /// Supported numeric value types for are , , , , , and . + public void EmitCounter(string name, T value, ReadOnlySpan> attributes, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Counter, name, value, null, attributes, scope); + } +} diff --git a/src/Sentry/SentryTraceMetrics.Distribution.cs b/src/Sentry/SentryTraceMetrics.Distribution.cs new file mode 100644 index 0000000000..b31d9bd626 --- /dev/null +++ b/src/Sentry/SentryTraceMetrics.Distribution.cs @@ -0,0 +1,73 @@ +namespace Sentry; + +public abstract partial class SentryTraceMetrics +{ + /// + /// Add a distribution value. + /// + /// The name of the metric. + /// The value of the metric. + /// The numeric type of the metric. + /// Supported numeric value types for are , , , , , and . + public void EmitDistribution(string name, T value) where T : struct + { + CaptureMetric(SentryMetricType.Distribution, name, value, null, [], null); + } + + /// + /// Add a distribution value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// The numeric type of the metric. + /// Supported numeric value types for are , , , , , and . + public void EmitDistribution(string name, T value, string? unit) where T : struct + { + CaptureMetric(SentryMetricType.Distribution, name, value, unit, [], null); + } + + /// + /// 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. + /// Supported numeric value types for are , , , , , and . + public void EmitDistribution(string name, T value, string? unit, Scope? scope) where T : struct + { + CaptureMetric(SentryMetricType.Distribution, name, value, unit, [], scope); + } + + /// + /// Add a distribution value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// A dictionary of attributes (key-value pairs with type information). + /// The scope to capture the metric with. + /// The numeric type of the metric. + /// Supported numeric value types for are , , , , , and . + public void EmitDistribution(string name, T value, string? unit, IEnumerable>? attributes, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Distribution, name, value, unit, attributes, scope); + } + + /// + /// Add a distribution value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// A dictionary of attributes (key-value pairs with type information). + /// The scope to capture the metric with. + /// The numeric type of the metric. + /// Supported numeric value types for are , , , , , and . + public void EmitDistribution(string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Distribution, name, value, unit, attributes, scope); + } +} diff --git a/src/Sentry/SentryTraceMetrics.Gauge.cs b/src/Sentry/SentryTraceMetrics.Gauge.cs new file mode 100644 index 0000000000..7d46ab1f90 --- /dev/null +++ b/src/Sentry/SentryTraceMetrics.Gauge.cs @@ -0,0 +1,73 @@ +namespace Sentry; + +public abstract partial class SentryTraceMetrics +{ + /// + /// Set a gauge value. + /// + /// The name of the metric. + /// The value of the metric. + /// The numeric type of the metric. + /// Supported numeric value types for are , , , , , and . + public void EmitGauge(string name, T value) where T : struct + { + CaptureMetric(SentryMetricType.Gauge, name, value, null, [], null); + } + + /// + /// Set a gauge value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// The numeric type of the metric. + /// Supported numeric value types for are , , , , , and . + public void EmitGauge(string name, T value, string? unit) where T : struct + { + CaptureMetric(SentryMetricType.Gauge, name, value, unit, [], null); + } + + /// + /// 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. + /// Supported numeric value types for are , , , , , and . + public void EmitGauge(string name, T value, string? unit, Scope? scope) 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. + /// Supported numeric value types for are , , , , , and . + public void EmitGauge(string name, T value, string? unit, IEnumerable>? attributes, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Gauge, name, value, unit, attributes, scope); + } + + /// + /// Set a gauge value. + /// + /// The name of the metric. + /// The value of the metric. + /// The unit of measurement. + /// A dictionary of attributes (key-value pairs with type information). + /// The scope to capture the metric with. + /// The numeric type of the metric. + /// Supported numeric value types for are , , , , , and . + public void EmitGauge(string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope = null) where T : struct + { + CaptureMetric(SentryMetricType.Gauge, name, value, unit, attributes, scope); + } +} 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/src/Sentry/SentryTraceMetricsCallbacks.cs b/src/Sentry/SentryTraceMetricsCallbacks.cs new file mode 100644 index 0000000000..d8da174890 --- /dev/null +++ b/src/Sentry/SentryTraceMetricsCallbacks.cs @@ -0,0 +1,98 @@ +using Sentry.Extensibility; + +namespace Sentry; + +#if NET6_0_OR_GREATER +/// +/// Similar to . +/// +#endif +internal sealed class SentryTraceMetricsCallbacks +{ + private Func, SentryMetric?> _beforeSendMetricByte; + private Func, SentryMetric?> _beforeSendMetricInt16; + private Func, SentryMetric?> _beforeSendMetricInt32; + private Func, SentryMetric?> _beforeSendMetricInt64; + private Func, SentryMetric?> _beforeSendMetricSingle; + private Func, SentryMetric?> _beforeSendMetricDouble; + + internal SentryTraceMetricsCallbacks() + { + _beforeSendMetricByte = static traceMetric => traceMetric; + _beforeSendMetricInt16 = static traceMetric => traceMetric; + _beforeSendMetricInt32 = static traceMetric => traceMetric; + _beforeSendMetricInt64 = static traceMetric => traceMetric; + _beforeSendMetricSingle = static traceMetric => traceMetric; + _beforeSendMetricDouble = static traceMetric => traceMetric; + } + + internal void Set(Func, SentryMetric?> beforeSendMetric, IDiagnosticLogger? diagnosticLogger) where T : struct + { + beforeSendMetric ??= static traceMetric => traceMetric; + + if (typeof(T) == typeof(long)) + { + _beforeSendMetricInt64 = (Func, SentryMetric?>)(object)beforeSendMetric; + } + else if (typeof(T) == typeof(double)) + { + _beforeSendMetricDouble = (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(short)) + { + _beforeSendMetricInt16 = (Func, SentryMetric?>)(object)beforeSendMetric; + } + else if (typeof(T) == typeof(byte)) + { + _beforeSendMetricByte = (Func, SentryMetric?>)(object)beforeSendMetric; + } + else + { + diagnosticLogger?.LogWarning("{0} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, and double.", typeof(T)); + } + } + + internal SentryMetric? Invoke(SentryMetric metric) where T : struct + { + if (typeof(T) == typeof(long)) + { + return (SentryMetric?)(object?)_beforeSendMetricInt64.Invoke((SentryMetric)(object)metric); + } + + if (typeof(T) == typeof(double)) + { + return (SentryMetric?)(object?)_beforeSendMetricDouble.Invoke((SentryMetric)(object)metric); + } + + if (typeof(T) == typeof(int)) + { + return (SentryMetric?)(object?)_beforeSendMetricInt32.Invoke((SentryMetric)(object)metric); + } + + if (typeof(T) == typeof(float)) + { + return (SentryMetric?)(object?)_beforeSendMetricSingle.Invoke((SentryMetric)(object)metric); + } + + if (typeof(T) == typeof(short)) + { + return (SentryMetric?)(object?)_beforeSendMetricInt16.Invoke((SentryMetric)(object)metric); + } + + if (typeof(T) == typeof(byte)) + { + return (SentryMetric?)(object?)_beforeSendMetricByte.Invoke((SentryMetric)(object)metric); + } + + Debug.Fail($"Unhandled Metric Type {typeof(T)}.", "This instruction should be unreachable."); + return null; + } +} diff --git a/src/Sentry/SentryUnits.cs b/src/Sentry/SentryUnits.cs new file mode 100644 index 0000000000..f4c1ee51b0 --- /dev/null +++ b/src/Sentry/SentryUnits.cs @@ -0,0 +1,178 @@ +namespace Sentry; + +/// +/// Supported units by Relay and Sentry. +/// Applies to , as well as attributes of and attributes of . +/// +/// +/// Contains the units currently supported by Relay and Sentry. +/// For Metrics and Attributes, currently, custom units are not allowed and will be set to "None" by Relay. +/// +/// +/// +public static class SentryUnits +{ + /// + /// Duration Units. + /// + public static class Duration + { + /// + /// Nanosecond unit (ns). + /// 10^-9 seconds. + /// + public static string Nanosecond { get; } = "nanosecond"; + + /// + /// Microsecond unit (μs). + /// 10^-6 seconds. + /// + public static string Microsecond { get; } = "microsecond"; + + /// + /// Millisecond unit (ms). + /// 10^-3 seconds. + /// + public static string Millisecond { get; } = "millisecond"; + + /// + /// Second unit (s). + /// + public static string Second { get; } = "second"; + + /// + /// Minute unit (min). + /// 60 seconds. + /// + public static string Minute { get; } = "minute"; + + /// + /// Hour unit (h). + /// 3_600 seconds. + /// + public static string Hour { get; } = "hour"; + + /// + /// Day unit (d). + /// 86_400 seconds. + /// + public static string Day { get; } = "day"; + + /// + /// Week unit (wk). + /// 604_800 seconds. + /// + public static string Week { get; } = "week"; + } + + /// + /// Information Units. + /// + /// + /// Note that there are computer systems with a different number of bits per byte. + /// + public static class Information + { + /// + /// Bit unit (b). + /// 1/8 of a byte. + /// + public static string Bit { get; } = "bit"; + + /// + /// Byte unit (B). + /// 8 bits. + /// + public static string Byte { get; } = "byte"; + + /// + /// Kilobyte unit (kB). + /// 10^3 bytes = 1_000 bytes (decimal). + /// + public static string Kilobyte { get; } = "kilobyte"; + + /// + /// Kibibyte unit (KiB). + /// 2^10 bytes = 1_024 bytes (binary). + /// + public static string Kibibyte { get; } = "kibibyte"; + + /// + /// Megabyte unit (MB). + /// 10^6 bytes = 1_000_000 bytes (decimal). + /// + public static string Megabyte { get; } = "megabyte"; + + /// + /// Mebibyte unit (MiB). + /// 2^20 bytes = 1_048_576 bytes (binary). + /// + public static string Mebibyte { get; } = "mebibyte"; + + /// + /// Gigabyte unit (GB). + /// 10^9 bytes = 1_000_000_000 bytes (decimal). + /// + public static string Gigabyte { get; } = "gigabyte"; + + /// + /// Gibibyte unit (GiB). + /// 2^30 bytes = 1_073_741_824 bytes (binary). + /// + public static string Gibibyte { get; } = "gibibyte"; + + /// + /// Terabyte unit (TB). + /// 10^12 bytes = 1_000_000_000_000 bytes (decimal). + /// + public static string Terabyte { get; } = "terabyte"; + + /// + /// Tebibyte unit (TiB). + /// 2^40 bytes = 1_099_511_627_776 bytes (binary). + /// + public static string Tebibyte { get; } = "tebibyte"; + + /// + /// Petabyte unit (PB). + /// 10^15 bytes = 1_000_000_000_000_000 bytes (decimal). + /// + public static string Petabyte { get; } = "petabyte"; + + /// + /// Pebibyte unit (PiB). + /// 2^50 bytes = 1_125_899_906_842_624 bytes (binary). + /// + public static string Pebibyte { get; } = "pebibyte"; + + /// + /// Exabyte unit (EB). + /// 10^18 bytes = 1_000_000_000_000_000_000 bytes (decimal). + /// + public static string Exabyte { get; } = "exabyte"; + + /// + /// Exbibyte unit (EiB). + /// 2^60 bytes = 1_152_921_504_606_846_976 bytes (binary). + /// + public static string Exbibyte { get; } = "exbibyte"; + } + + /// + /// Fraction Units. + /// + public static class Fraction + { + /// + /// Ratio unit. + /// Floating point fraction of 1. + /// + public static string Ratio { get; } = "ratio"; + + /// + /// Percent unit (%). + /// Ratio expressed as a fraction of 100. 100% equals a ratio of 1.0. + /// + public static string Percent { get; } = "percent"; + } +} diff --git a/test/Sentry.Compiler.Extensions.Tests/Analyzers/TraceConnectedMetricsAnalyzerTests.cs b/test/Sentry.Compiler.Extensions.Tests/Analyzers/TraceConnectedMetricsAnalyzerTests.cs new file mode 100644 index 0000000000..a07f701b55 --- /dev/null +++ b/test/Sentry.Compiler.Extensions.Tests/Analyzers/TraceConnectedMetricsAnalyzerTests.cs @@ -0,0 +1,252 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using Sentry.Compiler.Extensions.Analyzers; + +using Verifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier; + +namespace Sentry.Compiler.Extensions.Tests.Analyzers; + +public class TraceConnectedMetricsAnalyzerTests +{ + [Fact] + public async Task NoCode_NoDiagnostics() + { + await Verifier.VerifyAnalyzerAsync(""); + } + + [Fact] + public async Task NoInvocations_NoDiagnostics() + { + var test = new CSharpAnalyzerTest + { + TestState = + { + ReferenceAssemblies = TargetFramework.ReferenceAssemblies, + AdditionalReferences = { typeof(SentryTraceMetrics).Assembly }, + Sources = + { + """ + #nullable enable + using Sentry; + + public class AnalyzerTest + { + public void Init(SentryOptions options) + { + options.Experimental.EnableMetrics = false; + } + + public void Emit(IHub hub) + { + var metrics = SentrySdk.Experimental.Metrics; + + _ = metrics.GetType(); + + #pragma warning disable SENTRYTRACECONNECTEDMETRICS + _ = hub.Metrics.GetType(); + #pragma warning restore SENTRYTRACECONNECTEDMETRICS + + _ = SentrySdk.Experimental.Metrics.Equals(null); + _ = SentrySdk.Experimental.Metrics.GetHashCode(); + _ = SentrySdk.Experimental.Metrics.GetType(); + _ = SentrySdk.Experimental.Metrics.ToString(); + } + } + """ + }, + ExpectedDiagnostics = { }, + } + }; + + await test.RunAsync(); + } + + [Fact] + public async Task SupportedInvocations_NoDiagnostics() + { + var test = new CSharpAnalyzerTest + { + TestState = + { + ReferenceAssemblies = TargetFramework.ReferenceAssemblies, + AdditionalReferences = { typeof(SentryTraceMetrics).Assembly }, + Sources = + { + """ + #nullable enable + using Sentry; + + public class AnalyzerTest + { + public void Init(SentryOptions options) + { + options.Experimental.SetBeforeSendMetric(static SentryMetric? (SentryMetric metric) => metric); + options.Experimental.SetBeforeSendMetric(BeforeSendMetric); + options.Experimental.SetBeforeSendMetric(OnBeforeSendMetric); + } + + public void Emit(IHub hub) + { + var scope = new Scope(new SentryOptions()); + var metrics = SentrySdk.Experimental.Metrics; + + #pragma warning disable SENTRYTRACECONNECTEDMETRICS + metrics.EmitCounter("name", 1); + hub.Metrics.EmitCounter("name", 1f); + SentrySdk.Experimental.Metrics.EmitCounter("name", 1.1d, [], scope); + + metrics.EmitGauge("name", 2); + hub.Metrics.EmitGauge("name", 2f); + SentrySdk.Experimental.Metrics.EmitGauge("name", 2.2d, "unit", [], scope); + + metrics.EmitDistribution("name", 3); + hub.Metrics.EmitDistribution("name", 3f); + SentrySdk.Experimental.Metrics.EmitDistribution("name", 3.3d, "unit", [], scope); + #pragma warning restore SENTRYTRACECONNECTEDMETRICS + } + + private static SentryMetric? BeforeSendMetric(SentryMetric metric) where T : struct + { + return metric; + } + + private static SentryMetric? OnBeforeSendMetric(SentryMetric metric) + { + return metric; + } + } + + public static class Extensions + { + public static void EmitCounter(this SentryTraceMetrics metrics) where T : struct + { + metrics.EmitCounter("default", default(T), [], null); + } + + public static void EmitCounter(this SentryTraceMetrics metrics, string name) where T : struct + { + metrics.EmitCounter(name, default(T), [], null); + } + + public static void EmitGauge(this SentryTraceMetrics metrics) where T : struct + { + metrics.EmitGauge("default", default(T), null, [], null); + } + + public static void EmitGauge(this SentryTraceMetrics metrics, string name) where T : struct + { + metrics.EmitGauge(name, default(T), null, [], null); + } + + public static void EmitDistribution(this SentryTraceMetrics metrics) where T : struct + { + metrics.EmitDistribution("default", default(T), null, [], null); + } + + public static void EmitDistribution(this SentryTraceMetrics metrics, string name) where T : struct + { + metrics.EmitDistribution(name, default(T), null, [], null); + } + } + """ + }, + ExpectedDiagnostics = { }, + }, + SolutionTransforms = { SolutionTransforms.Nullable }, + }; + + await test.RunAsync(); + } + + [Fact] + public async Task UnsupportedInvocations_ReportDiagnostics() + { + var test = new CSharpAnalyzerTest + { + TestState = + { + ReferenceAssemblies = TargetFramework.ReferenceAssemblies, + AdditionalReferences = { typeof(SentryTraceMetrics).Assembly }, + Sources = + { + """ + #nullable enable + using System; + using Sentry; + + public class AnalyzerTest + { + public void Init(SentryOptions options) + { + {|#0:options.Experimental.SetBeforeSendMetric(static SentryMetric? (SentryMetric metric) => metric)|#0}; + {|#1:options.Experimental.SetBeforeSendMetric(BeforeSendMetric)|#1}; + {|#2:options.Experimental.SetBeforeSendMetric(OnBeforeSendMetric)|#2}; + } + + public void Emit(IHub hub) + { + var scope = new Scope(new SentryOptions()); + var metrics = SentrySdk.Experimental.Metrics; + + #pragma warning disable SENTRYTRACECONNECTEDMETRICS + {|#10:metrics.EmitCounter("name", (uint)1)|#10}; + {|#11:hub.Metrics.EmitCounter("name", (StringComparison)1f)|#11}; + {|#12:SentrySdk.Experimental.Metrics.EmitCounter("name", 1.1m, [], scope)|#12}; + + {|#13:metrics.EmitGauge("name", (uint)2)|#13}; + {|#14:hub.Metrics.EmitGauge("name", (StringComparison)2f)|#14}; + {|#15:SentrySdk.Experimental.Metrics.EmitGauge("name", 2.2m, "unit", [], scope)|#15}; + + {|#16:metrics.EmitDistribution("name", (uint)3)|#16}; + {|#17:hub.Metrics.EmitDistribution("name", (StringComparison)3f)|#17}; + {|#18:SentrySdk.Experimental.Metrics.EmitDistribution("name", 3.3m, "unit", [], scope)|#18}; + #pragma warning restore SENTRYTRACECONNECTEDMETRICS + } + + private static SentryMetric? BeforeSendMetric(SentryMetric metric) where T : struct + { + return metric; + } + + private static SentryMetric? OnBeforeSendMetric(SentryMetric metric) + { + return metric; + } + } + """ + }, + ExpectedDiagnostics = + { + CreateDiagnostic(0, typeof(sbyte)), + CreateDiagnostic(1, typeof(ushort)), + CreateDiagnostic(2, typeof(ulong)), + + CreateDiagnostic(10, typeof(uint)), + CreateDiagnostic(11, typeof(StringComparison)), + CreateDiagnostic(12, typeof(decimal)), + CreateDiagnostic(13, typeof(uint)), + CreateDiagnostic(14, typeof(StringComparison)), + CreateDiagnostic(15, typeof(decimal)), + CreateDiagnostic(16, typeof(uint)), + CreateDiagnostic(17, typeof(StringComparison)), + CreateDiagnostic(18, typeof(decimal)), + }, + } + }; + + await test.RunAsync(); + } + + private static DiagnosticResult CreateDiagnostic(int markupKey, Type type) + { + Assert.NotNull(type.FullName); + + return Verifier.Diagnostic("SENTRY1001") + .WithSeverity(DiagnosticSeverity.Warning) + .WithArguments(type.FullName) + .WithMessage($"{type.FullName} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, and double.") + .WithMessageFormat("{0} is unsupported type for Sentry Metrics. The only supported types are byte, short, int, long, float, and double.") + .WithLocation(markupKey); + } +} diff --git a/test/Sentry.Compiler.Extensions.Tests/Sentry.Compiler.Extensions.Tests.csproj b/test/Sentry.Compiler.Extensions.Tests/Sentry.Compiler.Extensions.Tests.csproj index 3265c3c21c..e005a20dc9 100644 --- a/test/Sentry.Compiler.Extensions.Tests/Sentry.Compiler.Extensions.Tests.csproj +++ b/test/Sentry.Compiler.Extensions.Tests/Sentry.Compiler.Extensions.Tests.csproj @@ -7,16 +7,20 @@ - - - - + + + + - + + + + + diff --git a/test/Sentry.Compiler.Extensions.Tests/SolutionTransforms.cs b/test/Sentry.Compiler.Extensions.Tests/SolutionTransforms.cs new file mode 100644 index 0000000000..bc4bfb44ce --- /dev/null +++ b/test/Sentry.Compiler.Extensions.Tests/SolutionTransforms.cs @@ -0,0 +1,37 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Sentry.Compiler.Extensions.Tests; + +internal static class SolutionTransforms +{ + private static readonly ImmutableDictionary s_nullableWarnings = GetNullableWarningsFromCompiler(); + + internal static Func Nullable { get; } = static (solution, projectId) => + { + var project = solution.GetProject(projectId); + Assert.NotNull(project); + + var compilationOptions = project.CompilationOptions; + Assert.NotNull(compilationOptions); + + compilationOptions = compilationOptions.WithSpecificDiagnosticOptions(compilationOptions.SpecificDiagnosticOptions.SetItems(s_nullableWarnings)); + + solution = solution.WithProjectCompilationOptions(projectId, compilationOptions); + return solution; + }; + + private static ImmutableDictionary GetNullableWarningsFromCompiler() + { + string[] args = { "/warnaserror:nullable" }; + var commandLineArguments = CSharpCommandLineParser.Default.Parse(args, Environment.CurrentDirectory, Environment.CurrentDirectory, null); + var nullableWarnings = commandLineArguments.CompilationOptions.SpecificDiagnosticOptions; + + // Workaround for https://github.com/dotnet/roslyn/issues/41610 + nullableWarnings = nullableWarnings + .SetItem("CS8632", ReportDiagnostic.Error) + .SetItem("CS8669", ReportDiagnostic.Error); + + return nullableWarnings; + } +} diff --git a/test/Sentry.Compiler.Extensions.Tests/TargetFramework.cs b/test/Sentry.Compiler.Extensions.Tests/TargetFramework.cs new file mode 100644 index 0000000000..3e8191b8c4 --- /dev/null +++ b/test/Sentry.Compiler.Extensions.Tests/TargetFramework.cs @@ -0,0 +1,23 @@ +using Microsoft.CodeAnalysis.Testing; + +namespace Sentry.Compiler.Extensions.Tests; + +internal static class TargetFramework +{ + internal static ReferenceAssemblies ReferenceAssemblies + { + get + { +#if NET8_0 + return ReferenceAssemblies.Net.Net80; +#elif NET9_0 + return ReferenceAssemblies.Net.Net90; +#elif NET10_0 + return ReferenceAssemblies.Net.Net100; +#else +#warning Target Framework not implemented. + throw new NotImplementedException(); +#endif + } + } +} 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) + { + var expected = Create(type, name, value, null, null, null); + Assert.Equal(expected, this); + } + + 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..3a08f11ba4 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", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] + 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,33 @@ 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 + { + [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, TAttribute value) + where TAttribute : notnull { } + public bool TryGetAttribute(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TAttribute value) { } + } public enum SentryMonitorInterval { Year = 0, @@ -715,6 +744,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 +832,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 +867,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 +931,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 +1052,40 @@ namespace Sentry public override string ToString() { } public static Sentry.SentryTraceHeader? Parse(string value) { } } + public abstract class SentryTraceMetrics + { + protected abstract void CaptureMetric(Sentry.SentryMetric metric) + where T : struct; + public void EmitCounter(string name, T value) + where T : struct { } + public void EmitCounter(string name, T value, Sentry.Scope? scope) + where T : struct { } + public void EmitCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitDistribution(string name, T value) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitGauge(string name, T value) + where T : struct { } + public void EmitGauge(string name, T value, string? unit) + where T : struct { } + public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope) + where T : struct { } + public void EmitGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + protected abstract void Flush(); + } 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) { } @@ -1060,6 +1135,42 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SentryTransaction FromJson(System.Text.Json.JsonElement json) { } } + public static class SentryUnits + { + public static class Duration + { + public static string Day { get; } + public static string Hour { get; } + public static string Microsecond { get; } + public static string Millisecond { get; } + public static string Minute { get; } + public static string Nanosecond { get; } + public static string Second { get; } + public static string Week { get; } + } + public static class Fraction + { + public static string Percent { get; } + public static string Ratio { get; } + } + public static class Information + { + public static string Bit { get; } + public static string Byte { get; } + public static string Exabyte { get; } + public static string Exbibyte { get; } + public static string Gibibyte { get; } + public static string Gigabyte { get; } + public static string Kibibyte { get; } + public static string Kilobyte { get; } + public static string Mebibyte { get; } + public static string Megabyte { get; } + public static string Pebibyte { get; } + public static string Petabyte { get; } + public static string Tebibyte { get; } + public static string Terabyte { get; } + } + } public sealed class SentryUser : Sentry.ISentryJsonSerializable { public SentryUser() { } @@ -1385,6 +1496,8 @@ namespace Sentry.Extensibility public bool IsSessionActive { get; } public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] + 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 +1545,8 @@ namespace Sentry.Extensibility public bool IsSessionActive { get; } public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] + 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..3a08f11ba4 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", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] + 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,33 @@ 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 + { + [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, TAttribute value) + where TAttribute : notnull { } + public bool TryGetAttribute(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TAttribute value) { } + } public enum SentryMonitorInterval { Year = 0, @@ -715,6 +744,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 +832,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 +867,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 +931,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 +1052,40 @@ namespace Sentry public override string ToString() { } public static Sentry.SentryTraceHeader? Parse(string value) { } } + public abstract class SentryTraceMetrics + { + protected abstract void CaptureMetric(Sentry.SentryMetric metric) + where T : struct; + public void EmitCounter(string name, T value) + where T : struct { } + public void EmitCounter(string name, T value, Sentry.Scope? scope) + where T : struct { } + public void EmitCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitDistribution(string name, T value) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitGauge(string name, T value) + where T : struct { } + public void EmitGauge(string name, T value, string? unit) + where T : struct { } + public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope) + where T : struct { } + public void EmitGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + protected abstract void Flush(); + } 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) { } @@ -1060,6 +1135,42 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SentryTransaction FromJson(System.Text.Json.JsonElement json) { } } + public static class SentryUnits + { + public static class Duration + { + public static string Day { get; } + public static string Hour { get; } + public static string Microsecond { get; } + public static string Millisecond { get; } + public static string Minute { get; } + public static string Nanosecond { get; } + public static string Second { get; } + public static string Week { get; } + } + public static class Fraction + { + public static string Percent { get; } + public static string Ratio { get; } + } + public static class Information + { + public static string Bit { get; } + public static string Byte { get; } + public static string Exabyte { get; } + public static string Exbibyte { get; } + public static string Gibibyte { get; } + public static string Gigabyte { get; } + public static string Kibibyte { get; } + public static string Kilobyte { get; } + public static string Mebibyte { get; } + public static string Megabyte { get; } + public static string Pebibyte { get; } + public static string Petabyte { get; } + public static string Tebibyte { get; } + public static string Terabyte { get; } + } + } public sealed class SentryUser : Sentry.ISentryJsonSerializable { public SentryUser() { } @@ -1385,6 +1496,8 @@ namespace Sentry.Extensibility public bool IsSessionActive { get; } public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] + 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 +1545,8 @@ namespace Sentry.Extensibility public bool IsSessionActive { get; } public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] + 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..3a08f11ba4 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", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] + 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,33 @@ 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 + { + [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, TAttribute value) + where TAttribute : notnull { } + public bool TryGetAttribute(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TAttribute value) { } + } public enum SentryMonitorInterval { Year = 0, @@ -715,6 +744,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 +832,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 +867,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 +931,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 +1052,40 @@ namespace Sentry public override string ToString() { } public static Sentry.SentryTraceHeader? Parse(string value) { } } + public abstract class SentryTraceMetrics + { + protected abstract void CaptureMetric(Sentry.SentryMetric metric) + where T : struct; + public void EmitCounter(string name, T value) + where T : struct { } + public void EmitCounter(string name, T value, Sentry.Scope? scope) + where T : struct { } + public void EmitCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitDistribution(string name, T value) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitGauge(string name, T value) + where T : struct { } + public void EmitGauge(string name, T value, string? unit) + where T : struct { } + public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope) + where T : struct { } + public void EmitGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + protected abstract void Flush(); + } 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) { } @@ -1060,6 +1135,42 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SentryTransaction FromJson(System.Text.Json.JsonElement json) { } } + public static class SentryUnits + { + public static class Duration + { + public static string Day { get; } + public static string Hour { get; } + public static string Microsecond { get; } + public static string Millisecond { get; } + public static string Minute { get; } + public static string Nanosecond { get; } + public static string Second { get; } + public static string Week { get; } + } + public static class Fraction + { + public static string Percent { get; } + public static string Ratio { get; } + } + public static class Information + { + public static string Bit { get; } + public static string Byte { get; } + public static string Exabyte { get; } + public static string Exbibyte { get; } + public static string Gibibyte { get; } + public static string Gigabyte { get; } + public static string Kibibyte { get; } + public static string Kilobyte { get; } + public static string Mebibyte { get; } + public static string Megabyte { get; } + public static string Pebibyte { get; } + public static string Petabyte { get; } + public static string Tebibyte { get; } + public static string Terabyte { get; } + } + } public sealed class SentryUser : Sentry.ISentryJsonSerializable { public SentryUser() { } @@ -1385,6 +1496,8 @@ namespace Sentry.Extensibility public bool IsSessionActive { get; } public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] + 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 +1545,8 @@ namespace Sentry.Extensibility public bool IsSessionActive { get; } public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } + [System.Diagnostics.CodeAnalysis.Experimental("SENTRYTRACECONNECTEDMETRICS", UrlFormat="https://github.com/getsentry/sentry-dotnet/discussions/4838")] + 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.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index f2b22fbfdb..eeb409de69 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -185,6 +185,7 @@ namespace Sentry bool IsSessionActive { get; } Sentry.SentryId LastEventId { get; } Sentry.SentryStructuredLogger Logger { get; } + 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); @@ -646,6 +647,27 @@ namespace Sentry protected abstract Sentry.ISpan? ProcessRequest(System.Net.Http.HttpRequestMessage request, string method, string url); 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} \\}")] + public sealed class SentryMetric + where T : struct + { + public string Name { get; init; } + public Sentry.SpanId? SpanId { get; init; } + public System.DateTimeOffset Timestamp { get; init; } + public Sentry.SentryId TraceId { get; init; } + public Sentry.SentryMetricType Type { get; init; } + public string? Unit { get; init; } + public T Value { get; init; } + public void SetAttribute(string key, TAttribute value) + where TAttribute : notnull { } + public bool TryGetAttribute(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TAttribute value) { } + } public enum SentryMonitorInterval { Year = 0, @@ -697,6 +719,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; } @@ -778,6 +801,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 { @@ -807,6 +836,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; } @@ -870,6 +900,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 { @@ -987,6 +1021,40 @@ namespace Sentry public override string ToString() { } public static Sentry.SentryTraceHeader? Parse(string value) { } } + public abstract class SentryTraceMetrics + { + protected abstract void CaptureMetric(Sentry.SentryMetric metric) + where T : struct; + public void EmitCounter(string name, T value) + where T : struct { } + public void EmitCounter(string name, T value, Sentry.Scope? scope) + where T : struct { } + public void EmitCounter(string name, T value, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitCounter(string name, T value, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitDistribution(string name, T value) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit, Sentry.Scope? scope) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitDistribution(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitGauge(string name, T value) + where T : struct { } + public void EmitGauge(string name, T value, string? unit) + where T : struct { } + public void EmitGauge(string name, T value, string? unit, Sentry.Scope? scope) + where T : struct { } + public void EmitGauge(string name, T value, string? unit, System.Collections.Generic.IEnumerable>? attributes, Sentry.Scope? scope = null) + where T : struct { } + public void EmitGauge(string name, T value, string? unit, System.ReadOnlySpan> attributes, Sentry.Scope? scope = null) + where T : struct { } + protected abstract void Flush(); + } 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) { } @@ -1036,6 +1104,42 @@ namespace Sentry public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } public static Sentry.SentryTransaction FromJson(System.Text.Json.JsonElement json) { } } + public static class SentryUnits + { + public static class Duration + { + public static string Day { get; } + public static string Hour { get; } + public static string Microsecond { get; } + public static string Millisecond { get; } + public static string Minute { get; } + public static string Nanosecond { get; } + public static string Second { get; } + public static string Week { get; } + } + public static class Fraction + { + public static string Percent { get; } + public static string Ratio { get; } + } + public static class Information + { + public static string Bit { get; } + public static string Byte { get; } + public static string Exabyte { get; } + public static string Exbibyte { get; } + public static string Gibibyte { get; } + public static string Gigabyte { get; } + public static string Kibibyte { get; } + public static string Kilobyte { get; } + public static string Mebibyte { get; } + public static string Megabyte { get; } + public static string Pebibyte { get; } + public static string Petabyte { get; } + public static string Tebibyte { get; } + public static string Terabyte { get; } + } + } public sealed class SentryUser : Sentry.ISentryJsonSerializable { public SentryUser() { } @@ -1361,6 +1465,7 @@ namespace Sentry.Extensibility public bool IsSessionActive { get; } public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } + 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) { } @@ -1408,6 +1513,7 @@ namespace Sentry.Extensibility public bool IsSessionActive { get; } public Sentry.SentryId LastEventId { get; } public Sentry.SentryStructuredLogger Logger { get; } + 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/Internals/StructuredLogBatchBufferTests.cs b/test/Sentry.Tests/Internals/BatchBufferTests.cs similarity index 82% rename from test/Sentry.Tests/Internals/StructuredLogBatchBufferTests.cs rename to test/Sentry.Tests/Internals/BatchBufferTests.cs index 77d8d34ebd..51cdec4512 100644 --- a/test/Sentry.Tests/Internals/StructuredLogBatchBufferTests.cs +++ b/test/Sentry.Tests/Internals/BatchBufferTests.cs @@ -2,7 +2,12 @@ namespace Sentry.Tests.Internals; -public class StructuredLogBatchBufferTests +/// +/// (formerly "Sentry.Internal.StructuredLogBatchBuffer") was originally developed as Batch Buffer for Logs only. +/// When adding support for Trace-connected Metrics, which are quite similar to Logs, it has been made generic to support both. +/// These tests are still using , rather than . +/// +public class BatchBufferTests { private sealed class Fixture { @@ -10,14 +15,14 @@ private sealed class Fixture public TimeSpan Timeout { get; set; } = System.Threading.Timeout.InfiniteTimeSpan; public string? Name { get; set; } - public List TimeoutExceededInvocations { get; } = []; + public List> TimeoutExceededInvocations { get; } = []; - public StructuredLogBatchBuffer GetSut() + public BatchBuffer GetSut() { - return new StructuredLogBatchBuffer(Capacity, Timeout, OnTimeoutExceeded, Name); + return new BatchBuffer(Capacity, Timeout, OnTimeoutExceeded, Name); } - private void OnTimeoutExceeded(StructuredLogBatchBuffer buffer) + private void OnTimeoutExceeded(BatchBuffer buffer) { TimeoutExceededInvocations.Add(buffer); } @@ -72,16 +77,16 @@ public void Add_CapacityTwo_CanAddTwice() buffer.Capacity.Should().Be(2); buffer.IsEmpty.Should().BeTrue(); - buffer.Add("one").Should().Be(StructuredLogBatchBufferAddStatus.AddedFirst); + buffer.Add("one").Should().Be(BatchBufferAddStatus.AddedFirst); buffer.IsEmpty.Should().BeFalse(); - buffer.Add("two").Should().Be(StructuredLogBatchBufferAddStatus.AddedLast); + buffer.Add("two").Should().Be(BatchBufferAddStatus.AddedLast); buffer.IsEmpty.Should().BeFalse(); - buffer.Add("three").Should().Be(StructuredLogBatchBufferAddStatus.IgnoredCapacityExceeded); + buffer.Add("three").Should().Be(BatchBufferAddStatus.IgnoredCapacityExceeded); buffer.IsEmpty.Should().BeFalse(); - buffer.Add("four").Should().Be(StructuredLogBatchBufferAddStatus.IgnoredCapacityExceeded); + buffer.Add("four").Should().Be(BatchBufferAddStatus.IgnoredCapacityExceeded); buffer.IsEmpty.Should().BeFalse(); } @@ -94,16 +99,16 @@ public void Add_CapacityThree_CanAddThrice() buffer.Capacity.Should().Be(3); buffer.IsEmpty.Should().BeTrue(); - buffer.Add("one").Should().Be(StructuredLogBatchBufferAddStatus.AddedFirst); + buffer.Add("one").Should().Be(BatchBufferAddStatus.AddedFirst); buffer.IsEmpty.Should().BeFalse(); - buffer.Add("two").Should().Be(StructuredLogBatchBufferAddStatus.Added); + buffer.Add("two").Should().Be(BatchBufferAddStatus.Added); buffer.IsEmpty.Should().BeFalse(); - buffer.Add("three").Should().Be(StructuredLogBatchBufferAddStatus.AddedLast); + buffer.Add("three").Should().Be(BatchBufferAddStatus.AddedLast); buffer.IsEmpty.Should().BeFalse(); - buffer.Add("four").Should().Be(StructuredLogBatchBufferAddStatus.IgnoredCapacityExceeded); + buffer.Add("four").Should().Be(BatchBufferAddStatus.IgnoredCapacityExceeded); buffer.IsEmpty.Should().BeFalse(); } @@ -115,12 +120,12 @@ public void Add_Flushing_CannotAdd() var flushScope = buffer.TryEnterFlushScope(); - buffer.Add("one").Should().Be(StructuredLogBatchBufferAddStatus.IgnoredIsFlushing); + buffer.Add("one").Should().Be(BatchBufferAddStatus.IgnoredIsFlushing); buffer.IsEmpty.Should().BeTrue(); flushScope.Dispose(); - buffer.Add("two").Should().Be(StructuredLogBatchBufferAddStatus.AddedFirst); + buffer.Add("two").Should().Be(BatchBufferAddStatus.AddedFirst); buffer.IsEmpty.Should().BeFalse(); } @@ -132,7 +137,7 @@ public void Add_Disposed_CannotAdd() buffer.Dispose(); - buffer.Add("one").Should().Be(StructuredLogBatchBufferAddStatus.IgnoredIsDisposed); + buffer.Add("one").Should().Be(BatchBufferAddStatus.IgnoredIsDisposed); buffer.IsEmpty.Should().BeTrue(); } @@ -311,7 +316,7 @@ public void OnIntervalElapsed_Disposed_DoesNotInvokeCallback() } // cannot use xUnit's Throws() nor Fluent Assertions' ThrowExactly() because the FlushScope is a ref struct - private static void AssertFlushThrows(StructuredLogBatchBuffer.FlushScope flushScope) + private static void AssertFlushThrows(BatchBuffer.FlushScope flushScope) where T : Exception { Exception? exception = null; @@ -329,9 +334,9 @@ private static void AssertFlushThrows(StructuredLogBatchBuffer.FlushScope flu } } -file static class StructuredLogBatchBufferHelpers +file static class BatchBufferHelpers { - public static StructuredLogBatchBufferAddStatus Add(this StructuredLogBatchBuffer buffer, string item) + public static BatchBufferAddStatus Add(this BatchBuffer buffer, string item) { SentryLog log = new(DateTimeOffset.MinValue, SentryId.Empty, SentryLogLevel.Trace, item); return buffer.Add(log); diff --git a/test/Sentry.Tests/Internals/StructuredLogBatchProcessorTests.cs b/test/Sentry.Tests/Internals/BatchProcessorTests.cs similarity index 91% rename from test/Sentry.Tests/Internals/StructuredLogBatchProcessorTests.cs rename to test/Sentry.Tests/Internals/BatchProcessorTests.cs index 12279f6115..83b1feca7f 100644 --- a/test/Sentry.Tests/Internals/StructuredLogBatchProcessorTests.cs +++ b/test/Sentry.Tests/Internals/BatchProcessorTests.cs @@ -2,7 +2,12 @@ namespace Sentry.Tests.Internals; -public class StructuredLogBatchProcessorTests : IDisposable +/// +/// (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. +/// These tests are still using , rather than . +/// +public class BatchProcessorTests : IDisposable { private sealed class Fixture { @@ -35,9 +40,9 @@ public void DisableHub() _hub.IsEnabled.Returns(false); } - public StructuredLogBatchProcessor GetSut(int batchCount) + public BatchProcessor GetSut(int batchCount) { - return new StructuredLogBatchProcessor(_hub, batchCount, Timeout.InfiniteTimeSpan, ClientReportRecorder, DiagnosticLogger); + return new BatchProcessor(_hub, batchCount, Timeout.InfiniteTimeSpan, StructuredLog.Capture, ClientReportRecorder, DiagnosticLogger); } } @@ -128,7 +133,7 @@ public async Task Enqueue_Concurrency_CaptureEnvelopes() { tasks[i] = Task.Factory.StartNew(static state => { - var (sync, logsPerTask, taskIndex, processor) = ((ManualResetEvent, int, int, StructuredLogBatchProcessor))state!; + var (sync, logsPerTask, taskIndex, processor) = ((ManualResetEvent, int, int, BatchProcessor))state!; sync.WaitOne(5_000); for (var i = 0; i < logsPerTask; i++) { diff --git a/test/Sentry.Tests/SentryStructuredLoggerTests.cs b/test/Sentry.Tests/SentryStructuredLoggerTests.cs index 008a56df19..3a038fd840 100644 --- a/test/Sentry.Tests/SentryStructuredLoggerTests.cs +++ b/test/Sentry.Tests/SentryStructuredLoggerTests.cs @@ -240,9 +240,9 @@ public void Dispose_BeforeLog_DoesNotCaptureEnvelope() _fixture.Hub.Received(0).CaptureEnvelope(Arg.Any()); var entry = _fixture.DiagnosticLogger.Dequeue(); entry.Level.Should().Be(SentryLevel.Info); - entry.Message.Should().Be("Log Buffer full ... dropping log"); + entry.Message.Should().Be("{0}-Buffer full ... dropping {0}"); entry.Exception.Should().BeNull(); - entry.Args.Should().BeEmpty(); + entry.Args.Should().BeEquivalentTo([nameof(SentryLog)]); } private static void ConfigureLog(SentryLog log)