From 741e0887fb4b38effa133eeea7d0c8430f2b40fb Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 30 Jan 2024 11:34:30 +1300 Subject: [PATCH 1/2] Added StartTimer extension method to IMetricAggregator (#3075) --- CHANGELOG.md | 4 ++ .../Sentry.Samples.Console.Metrics/Program.cs | 32 ++++----- src/Sentry/DisabledMetricAggregator.cs | 19 ++++++ src/Sentry/Extensibility/DisabledHub.cs | 11 +++ src/Sentry/Extensibility/HubAdapter.cs | 5 ++ src/Sentry/IHub.cs | 7 ++ src/Sentry/IMetricAggregator.cs | 12 ++++ src/Sentry/IMetricHub.cs | 21 ++++++ src/Sentry/ISentryClient.cs | 15 ++-- src/Sentry/Internal/Hub.cs | 68 ++++++++++++++++++- src/Sentry/MetricAggregator.cs | 34 ++++------ src/Sentry/Protocol/Metrics/CodeLocations.cs | 4 ++ src/Sentry/Protocol/Metrics/Metric.cs | 50 ++++++++++++-- .../Metrics/MetricResourceIdentifier.cs | 9 +++ src/Sentry/Protocol/Metrics/MetricType.cs | 15 +++- src/Sentry/Protocol/Metrics/SetMetric.cs | 2 +- src/Sentry/SentryClient.cs | 48 ++----------- src/Sentry/SentrySdk.cs | 8 +++ src/Sentry/Timing.cs | 48 ++++--------- ...piApprovalTests.Run.DotNet6_0.verified.txt | 15 ++-- ...piApprovalTests.Run.DotNet7_0.verified.txt | 15 ++-- ...piApprovalTests.Run.DotNet8_0.verified.txt | 15 ++-- .../ApiApprovalTests.Run.Net4_8.verified.txt | 15 ++-- test/Sentry.Tests/MetricAggregatorTests.cs | 17 ++--- 24 files changed, 321 insertions(+), 168 deletions(-) create mode 100644 src/Sentry/IMetricHub.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d9cf8dff4..f5a9089dd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ If you have conflicts, you can opt-out by adding the following to your `csproj`: ``` +### Features + +- Timing metrics can now be captured with `SentrySdk.Metrics.StartTimer` ([#3075](https://github.com/getsentry/sentry-dotnet/pull/3075)) + ### Fixes - Fixed an issue with tag values in metrics not being properly serialized ([#3065](https://github.com/getsentry/sentry-dotnet/pull/3065)) diff --git a/samples/Sentry.Samples.Console.Metrics/Program.cs b/samples/Sentry.Samples.Console.Metrics/Program.cs index 09c7b3246e..7854b6e39a 100644 --- a/samples/Sentry.Samples.Console.Metrics/Program.cs +++ b/samples/Sentry.Samples.Console.Metrics/Program.cs @@ -15,6 +15,8 @@ private static void Main() options.Debug = true; options.StackTraceMode = StackTraceMode.Enhanced; + options.SampleRate = 1.0f; // Not recommended in production - may adversely impact quota + options.TracesSampleRate = 1.0f; // Not recommended in production - may adversely impact quota // Initialize some (non null) ExperimentalMetricsOptions to enable Sentry Metrics, options.ExperimentalMetrics = new ExperimentalMetricsOptions { @@ -24,22 +26,17 @@ private static void Main() })) { System.Console.WriteLine("Measure, Yeah, Measure!"); + Action[] actions = + [ + () => PlaySetBingo(10), + () => CreateRevenueGauge(100), + () => MeasureShrimp(30), + ]; while (true) { // Perform your task here - switch (Roll.Next(1,3)) - { - case 1: - PlaySetBingo(10); - break; - case 2: - CreateRevenueGauge(100); - break; - case 3: - MeasureShrimp(30); - break; - } - + var actionIdx = Roll.Next(0, actions.Length); + actions[actionIdx](); // Optional: Delay to prevent tight looping var sleepTime = Roll.Next(1, 10); @@ -60,9 +57,10 @@ private static void PlaySetBingo(int attempts) { var solution = new[] { 3, 5, 7, 11, 13, 17 }; - // The Timing class creates a distribution that is designed to measure the amount of time it takes to run code + // StartTimer creates a distribution that is designed to measure the amount of time it takes to run code // blocks. By default it will use a unit of Seconds - we're configuring it to use milliseconds here though. - using (new Timing("bingo", MeasurementUnit.Duration.Millisecond)) + // The return value is an IDisposable and the timer will stop when the timer is disposed of. + using (SentrySdk.Metrics.StartTimer("bingo", MeasurementUnit.Duration.Millisecond)) { for (var i = 0; i < attempts; i++) { @@ -78,7 +76,7 @@ private static void PlaySetBingo(int attempts) private static void CreateRevenueGauge(int sampleCount) { - using (new Timing(nameof(CreateRevenueGauge), MeasurementUnit.Duration.Millisecond)) + using (SentrySdk.Metrics.StartTimer(nameof(CreateRevenueGauge), MeasurementUnit.Duration.Millisecond)) { for (var i = 0; i < sampleCount; i++) { @@ -92,7 +90,7 @@ private static void CreateRevenueGauge(int sampleCount) private static void MeasureShrimp(int sampleCount) { - using (new Timing(nameof(MeasureShrimp), MeasurementUnit.Duration.Millisecond)) + using (SentrySdk.Metrics.StartTimer(nameof(MeasureShrimp), MeasurementUnit.Duration.Millisecond)) { for (var i = 0; i < sampleCount; i++) { diff --git a/src/Sentry/DisabledMetricAggregator.cs b/src/Sentry/DisabledMetricAggregator.cs index 61304898cf..eadecd07b8 100644 --- a/src/Sentry/DisabledMetricAggregator.cs +++ b/src/Sentry/DisabledMetricAggregator.cs @@ -37,6 +37,14 @@ public void Timing(string key, double value, MeasurementUnit.Duration unit = Mea // No Op } + public IDisposable StartTimer(string key, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, + IDictionary? tags = null, + int stackLevel = 1) + { + // No Op + return NoOpDisposable.Instance; + } + public Task FlushAsync(bool force = true, CancellationToken cancellationToken = default) { // No Op @@ -48,3 +56,14 @@ public void Dispose() // No Op } } + +internal class NoOpDisposable : IDisposable +{ + private static readonly Lazy LazyInstance = new(); + internal static NoOpDisposable Instance => LazyInstance.Value; + + public void Dispose() + { + // No Op + } +} diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs index bd12084c72..239d860942 100644 --- a/src/Sentry/Extensibility/DisabledHub.cs +++ b/src/Sentry/Extensibility/DisabledHub.cs @@ -1,3 +1,6 @@ +using Sentry.Protocol.Envelopes; +using Sentry.Protocol.Metrics; + namespace Sentry.Extensibility; /// @@ -133,6 +136,14 @@ public void BindClient(ISentryClient client) { } + /// + /// No-Op. + /// + public bool CaptureEnvelope(Envelope envelope) + { + return false; + } + /// /// No-Op. /// diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs index 1935ede45b..239b072fd5 100644 --- a/src/Sentry/Extensibility/HubAdapter.cs +++ b/src/Sentry/Extensibility/HubAdapter.cs @@ -1,4 +1,6 @@ using Sentry.Infrastructure; +using Sentry.Protocol.Envelopes; +using Sentry.Protocol.Metrics; namespace Sentry.Extensibility; @@ -209,6 +211,9 @@ public SentryId CaptureEvent(SentryEvent evt) public SentryId CaptureEvent(SentryEvent evt, Scope? scope) => SentrySdk.CaptureEvent(evt, scope, null); + /// + public bool CaptureEnvelope(Envelope envelope) => SentrySdk.CurrentHub.CaptureEnvelope(envelope); + /// /// Forwards the call to . /// diff --git a/src/Sentry/IHub.cs b/src/Sentry/IHub.cs index 726d548a7a..5a55a70a49 100644 --- a/src/Sentry/IHub.cs +++ b/src/Sentry/IHub.cs @@ -1,3 +1,5 @@ +using Sentry.Protocol.Metrics; + namespace Sentry; /// @@ -19,6 +21,11 @@ public interface IHub : /// SentryId LastEventId { get; } + /// + /// + /// + IMetricAggregator Metrics { get; } + /// /// Starts a transaction. /// diff --git a/src/Sentry/IMetricAggregator.cs b/src/Sentry/IMetricAggregator.cs index 04bc946b97..25c25905bf 100644 --- a/src/Sentry/IMetricAggregator.cs +++ b/src/Sentry/IMetricAggregator.cs @@ -96,6 +96,18 @@ void Timing(string key, DateTimeOffset? timestamp = null, int stackLevel = 1); + /// + /// Measures the time it takes to run a given code block and emits this as a metric. + /// + /// + /// using (SentrySdk.Metrics.StartTimer("my-operation")) + /// { + /// ... + /// } + /// + IDisposable StartTimer(string key, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, + IDictionary? tags = null, int stackLevel = 1); + /// /// Flushes any flushable metrics and/or code locations. /// If is true then the cutoff is ignored and all metrics are flushed. diff --git a/src/Sentry/IMetricHub.cs b/src/Sentry/IMetricHub.cs new file mode 100644 index 0000000000..861793b086 --- /dev/null +++ b/src/Sentry/IMetricHub.cs @@ -0,0 +1,21 @@ +using Sentry.Protocol.Metrics; + +namespace Sentry; + +internal interface IMetricHub +{ + /// + /// Captures one or more metrics to be sent to Sentry. + /// + void CaptureMetrics(IEnumerable metrics); + + /// + /// Captures one or more to be sent to Sentry. + /// + void CaptureCodeLocations(CodeLocations codeLocations); + + /// + /// Starts a child span for the current transaction or, if there is no active transaction, starts a new transaction. + /// + ISpan StartSpan(string operation, string description); +} diff --git a/src/Sentry/ISentryClient.cs b/src/Sentry/ISentryClient.cs index d676448a1a..56d41b565b 100644 --- a/src/Sentry/ISentryClient.cs +++ b/src/Sentry/ISentryClient.cs @@ -1,3 +1,6 @@ +using Sentry.Protocol.Envelopes; +using Sentry.Protocol.Metrics; + namespace Sentry; /// @@ -10,6 +13,13 @@ public interface ISentryClient /// bool IsEnabled { get; } + /// + /// Capture an envelope and queue it. + /// + /// The envelope. + /// true if the enveloped was queued, false otherwise. + bool CaptureEnvelope(Envelope envelope); + /// /// Capture the event /// @@ -68,9 +78,4 @@ public interface ISentryClient /// The amount of time allowed for flushing. /// A task to await for the flush operation. Task FlushAsync(TimeSpan timeout); - - /// - /// - /// - IMetricAggregator Metrics { get; } } diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index a047c0eaaf..a1ce81b56c 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -1,9 +1,11 @@ using Sentry.Extensibility; using Sentry.Infrastructure; +using Sentry.Protocol.Envelopes; +using Sentry.Protocol.Metrics; namespace Sentry.Internal; -internal class Hub : IHub, IDisposable +internal class Hub : IHub, IMetricHub, IDisposable { private readonly object _sessionPauseLock = new(); @@ -59,7 +61,14 @@ internal Hub( PushScope(); } - Metrics = _ownedClient.Metrics; + if (options.ExperimentalMetrics is not null) + { + Metrics = new MetricAggregator(options, this); + } + else + { + Metrics = new DisabledMetricAggregator(); + } foreach (var integration in options.Integrations) { @@ -394,6 +403,8 @@ public SentryId CaptureEvent(SentryEvent evt, Hint? hint, Action configur } } + public bool CaptureEnvelope(Envelope envelope) => _ownedClient.CaptureEnvelope(envelope); + public SentryId CaptureEvent(SentryEvent evt, Scope? scope = null, Hint? hint = null) { if (!IsEnabled) @@ -486,6 +497,57 @@ public void CaptureTransaction(SentryTransaction transaction, Scope? scope, Hint } } + /// + public void CaptureMetrics(IEnumerable metrics) + { + if (!IsEnabled) + { + return; + } + + Metric[]? enumerable = null; + try + { + enumerable = metrics as Metric[] ?? metrics.ToArray(); + _options.LogDebug("Capturing metrics."); + _ownedClient.CaptureEnvelope(Envelope.FromMetrics(metrics)); + } + catch (Exception e) + { + var metricEventIds = enumerable?.Select(m => m.EventId).ToArray() ?? []; + _options.LogError(e, "Failure to capture metrics: {0}", string.Join(",", metricEventIds)); + } + } + + /// + public void CaptureCodeLocations(CodeLocations codeLocations) + { + if (!IsEnabled) + { + return; + } + + try + { + _options.LogDebug("Capturing code locations for period: {0}", codeLocations.Timestamp); + _ownedClient.CaptureEnvelope(Envelope.FromCodeLocations(codeLocations)); + } + catch (Exception e) + { + _options.LogError(e, "Failure to capture code locations"); + } + } + + /// + public ISpan StartSpan(string operation, string description) + { + ITransactionTracer? currentTransaction = null; + ConfigureScope(s => currentTransaction = s.Transaction); + return currentTransaction is {} transaction + ? transaction.StartChild(operation, description) + : this.StartTransaction(operation, description); + } + public void CaptureSession(SessionUpdate sessionUpdate) { if (!IsEnabled) @@ -527,7 +589,7 @@ public void Dispose() try { - _ownedClient.Metrics.FlushAsync().ContinueWith(_ => + Metrics.FlushAsync().ContinueWith(_ => _ownedClient.FlushAsync(_options.ShutdownTimeout).Wait() ).ConfigureAwait(false).GetAwaiter().GetResult(); } diff --git a/src/Sentry/MetricAggregator.cs b/src/Sentry/MetricAggregator.cs index 26dce5e234..16f9e8416f 100644 --- a/src/Sentry/MetricAggregator.cs +++ b/src/Sentry/MetricAggregator.cs @@ -8,8 +8,7 @@ namespace Sentry; internal class MetricAggregator : IMetricAggregator { private readonly SentryOptions _options; - private readonly Action> _captureMetrics; - private readonly Action _captureCodeLocations; + private readonly IMetricHub _metricHub; private readonly TimeSpan _flushInterval; private readonly SemaphoreSlim _codeLocationLock = new(1,1); @@ -32,24 +31,12 @@ private readonly Lazy>> _b private readonly Task _loopTask; - /// - /// MetricAggregator constructor. - /// - /// The - /// The callback to be called to transmit aggregated metrics - /// The callback to be called to transmit new code locations - /// A - /// - /// A boolean value indicating whether the Loop to flush metrics should run, for testing only. - /// - /// An optional flushInterval, for testing only - internal MetricAggregator(SentryOptions options, Action> captureMetrics, - Action captureCodeLocations, CancellationTokenSource? shutdownSource = null, + internal MetricAggregator(SentryOptions options, IMetricHub metricHub, + CancellationTokenSource? shutdownSource = null, bool disableLoopTask = false, TimeSpan? flushInterval = null) { _options = options; - _captureMetrics = captureMetrics; - _captureCodeLocations = captureCodeLocations; + _metricHub = metricHub; _shutdownSource = shutdownSource ?? new CancellationTokenSource(); _flushInterval = flushInterval ?? TimeSpan.FromSeconds(5); @@ -161,6 +148,11 @@ public void Timing(string key, DateTimeOffset? timestamp = null, int stackLevel = 1) => Emit(MetricType.Distribution, key, value, unit, tags, timestamp, stackLevel + 1); + /// + public IDisposable StartTimer(string key, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, + IDictionary? tags = null, int stackLevel = 1) + => new Timing(this, _metricHub, _options, key, unit, tags, stackLevel + 1); + private void Emit( MetricType type, string key, @@ -231,7 +223,7 @@ private ConcurrentDictionary GetOrAddTimeBucket(long bucketKey) { return existingBucket; } - + var timeBucket = new ConcurrentDictionary(); Buckets[bucketKey] = timeBucket; return timeBucket; @@ -247,7 +239,7 @@ private ConcurrentDictionary GetOrAddTimeBucket(long bucketKey) } } - internal void RecordCodeLocation( + internal virtual void RecordCodeLocation( MetricType type, string key, MeasurementUnit unit, @@ -381,7 +373,7 @@ public async Task FlushAsync(bool force = true, CancellationToken cancellationTo _bucketsLock.ExitWriteLock(); } - _captureMetrics(bucket.Values); + _metricHub.CaptureMetrics(bucket.Values); _options.LogDebug("Metric flushed for bucket {0}", key); } @@ -391,7 +383,7 @@ public async Task FlushAsync(bool force = true, CancellationToken cancellationTo _options.LogDebug("Flushing code locations: ", timestamp); var codeLocations = new CodeLocations(timestamp, locations); - _captureCodeLocations(codeLocations); + _metricHub.CaptureCodeLocations(codeLocations); _options.LogDebug("Code locations flushed: ", timestamp); } diff --git a/src/Sentry/Protocol/Metrics/CodeLocations.cs b/src/Sentry/Protocol/Metrics/CodeLocations.cs index a8bf10e2f7..914170381b 100644 --- a/src/Sentry/Protocol/Metrics/CodeLocations.cs +++ b/src/Sentry/Protocol/Metrics/CodeLocations.cs @@ -3,6 +3,9 @@ namespace Sentry.Protocol.Metrics; +/// +/// Represents a collection of code locations. +/// internal class CodeLocations(long timestamp, IReadOnlyDictionary locations) : IJsonSerializable { @@ -12,6 +15,7 @@ internal class CodeLocations(long timestamp, IReadOnlyDictionary public long Timestamp => timestamp; + /// public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) { writer.WriteStartObject(); diff --git a/src/Sentry/Protocol/Metrics/Metric.cs b/src/Sentry/Protocol/Metrics/Metric.cs index 801f3c9173..87a71f12b4 100644 --- a/src/Sentry/Protocol/Metrics/Metric.cs +++ b/src/Sentry/Protocol/Metrics/Metric.cs @@ -4,12 +4,25 @@ namespace Sentry.Protocol.Metrics; +/// +/// Base class for metric instruments +/// internal abstract class Metric : IJsonSerializable, ISentrySerializable { + /// + /// Creates a new instance of . + /// protected Metric() : this(string.Empty) { } + /// + /// Creates a new instance of . + /// + /// The text key to be used to identify the metric + /// An optional that describes the values being tracked + /// An optional set of key/value paris that can be used to add dimensionality to metrics + /// An optional time when the metric was emitted. Defaults to DateTimeOffset.UtcNow protected Metric(string key, MeasurementUnit? unit = null, IDictionary? tags = null, DateTimeOffset? timestamp = null) { Key = key; @@ -18,16 +31,31 @@ protected Metric(string key, MeasurementUnit? unit = null, IDictionary + /// + /// + public SentryId EventId { get; } = SentryId.Create(); - public string Key { get; private set; } + /// + /// A text key identifying the metric + /// + public string Key { get; } - public DateTimeOffset Timestamp { get; private set; } + /// + /// The time when the metric was emitted. + /// + public DateTimeOffset Timestamp { get; } - public MeasurementUnit? Unit { get; private set; } + /// + /// A that describes the values being tracked + /// + public MeasurementUnit? Unit { get; } private IDictionary? _tags; + /// + /// A set of key/value paris providing dimensionality for the metric + /// public IDictionary Tags { get @@ -37,10 +65,17 @@ public IDictionary Tags } } + /// + /// Adds a value to the metric + /// public abstract void Add(double value); + /// + /// Serializes metric values to JSON + /// protected abstract void WriteValues(Utf8JsonWriter writer, IDiagnosticLogger? logger); + /// public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) { writer.WriteStartObject(); @@ -57,8 +92,14 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) writer.WriteEndObject(); } + /// + /// Concrete classes should implement this to return a list of values that should be serialized to statsd + /// protected abstract IEnumerable SerializedStatsdValues(); + /// + /// Serializes the metric asynchrounously in statsd format to the provided stream + /// public async Task SerializeAsync(Stream stream, IDiagnosticLogger? logger, CancellationToken cancellationToken = default) { /* @@ -111,6 +152,7 @@ async Task Write(string content) } } + /// public void Serialize(Stream stream, IDiagnosticLogger? logger) { SerializeAsync(stream, logger).GetAwaiter().GetResult(); diff --git a/src/Sentry/Protocol/Metrics/MetricResourceIdentifier.cs b/src/Sentry/Protocol/Metrics/MetricResourceIdentifier.cs index 3cd3a3906e..846f77c93f 100644 --- a/src/Sentry/Protocol/Metrics/MetricResourceIdentifier.cs +++ b/src/Sentry/Protocol/Metrics/MetricResourceIdentifier.cs @@ -1,7 +1,16 @@ namespace Sentry.Protocol.Metrics; +/// +/// Uniquely identifies a metric resource. +/// +/// +/// +/// internal record struct MetricResourceIdentifier(MetricType MetricType, string Key, MeasurementUnit Unit) { + /// + /// Returns a string representation of the metric resource identifier. + /// public override string ToString() => $"{MetricType.ToStatsdType()}:{MetricHelper.SanitizeKey(Key)}@{Unit}"; } diff --git a/src/Sentry/Protocol/Metrics/MetricType.cs b/src/Sentry/Protocol/Metrics/MetricType.cs index c323b914dd..0e624885a0 100644 --- a/src/Sentry/Protocol/Metrics/MetricType.cs +++ b/src/Sentry/Protocol/Metrics/MetricType.cs @@ -1,6 +1,19 @@ namespace Sentry.Protocol.Metrics; -internal enum MetricType : byte { Counter, Gauge, Distribution, Set } +/// +/// The metric instrument type +/// +internal enum MetricType : byte +{ + /// + Counter, + /// + Gauge, + /// + Distribution, + /// + Set +} internal static class MetricTypeExtensions { diff --git a/src/Sentry/Protocol/Metrics/SetMetric.cs b/src/Sentry/Protocol/Metrics/SetMetric.cs index ac102bdf37..53a42108cb 100644 --- a/src/Sentry/Protocol/Metrics/SetMetric.cs +++ b/src/Sentry/Protocol/Metrics/SetMetric.cs @@ -30,5 +30,5 @@ protected override void WriteValues(Utf8JsonWriter writer, IDiagnosticLogger? lo writer.WriteArrayIfNotEmpty("value", _value, logger); protected override IEnumerable SerializedStatsdValues() - => _value.Select(v => (IConvertible)v); + => _value.Cast(); } diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index 4c1d52e07e..e7cd5ad280 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -23,11 +23,6 @@ public class SentryClient : ISentryClient, IDisposable internal IBackgroundWorker Worker { get; } - /// - /// - /// - public IMetricAggregator Metrics { get; } - internal SentryOptions Options => _options; /// @@ -77,15 +72,6 @@ internal SentryClient( options.LogDebug("Worker of type {0} was provided via Options.", worker.GetType().Name); Worker = worker; } - - if (options.ExperimentalMetrics is not null) - { - Metrics = new MetricAggregator(options, CaptureMetrics, CaptureCodeLocations); - } - else - { - Metrics = new DisabledMetricAggregator(); - } } /// @@ -241,24 +227,6 @@ public void CaptureTransaction(SentryTransaction transaction, Scope? scope, Hint return transaction; } - /// - /// Captures one or more metrics to be sent to Sentry. - /// - internal void CaptureMetrics(IEnumerable metrics) - { - _options.LogDebug("Capturing metrics."); - CaptureEnvelope(Envelope.FromMetrics(metrics)); - } - - /// - /// Captures one or more to be sent to Sentry. - /// - internal void CaptureCodeLocations(CodeLocations codeLocations) - { - _options.LogDebug("Capturing code locations for period: {0}", codeLocations.Timestamp); - CaptureEnvelope(Envelope.FromCodeLocations(codeLocations)); - } - /// public void CaptureSession(SessionUpdate sessionUpdate) { @@ -403,12 +371,8 @@ private SentryId DoSendEvent(SentryEvent @event, Hint? hint, Scope? scope) return null; } - /// - /// Capture an envelope and queue it. - /// - /// The envelope. - /// true if the enveloped was queued, false otherwise. - private bool CaptureEnvelope(Envelope envelope) + /// + public bool CaptureEnvelope(Envelope envelope) { if (Worker.EnqueueEnvelope(envelope)) { @@ -471,14 +435,12 @@ public void Dispose() try { - Metrics.FlushAsync().ContinueWith(_ => - // Worker should empty it's queue until SentryOptions.ShutdownTimeout - Worker.FlushAsync(_options.ShutdownTimeout) - ).ConfigureAwait(false).GetAwaiter().GetResult(); + // Worker should empty its queue until SentryOptions.ShutdownTimeout + Worker.FlushAsync(_options.ShutdownTimeout).ConfigureAwait(false).GetAwaiter().GetResult(); } catch { - _options.LogDebug("Failed to wait on metrics/worker to flush"); + _options.LogDebug("Failed to wait on worker to flush"); } } } diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index 0d2916405e..ce29f57473 100644 --- a/src/Sentry/SentrySdk.cs +++ b/src/Sentry/SentrySdk.cs @@ -1,6 +1,8 @@ using Sentry.Extensibility; using Sentry.Infrastructure; using Sentry.Internal; +using Sentry.Protocol.Envelopes; +using Sentry.Protocol.Metrics; namespace Sentry; @@ -375,6 +377,12 @@ public static void ConfigureScope(Action configureScope) public static Task ConfigureScopeAsync(Func configureScope) => CurrentHub.ConfigureScopeAsync(configureScope); + /// + [DebuggerStepThrough] + [EditorBrowsable(EditorBrowsableState.Never)] + public static bool CaptureEnvelope(Envelope envelope) + => CurrentHub.CaptureEnvelope(envelope); + /// /// Captures the event, passing a hint, using the specified scope. /// diff --git a/src/Sentry/Timing.cs b/src/Sentry/Timing.cs index bdd206d2b1..d5052b4d9c 100644 --- a/src/Sentry/Timing.cs +++ b/src/Sentry/Timing.cs @@ -4,8 +4,7 @@ namespace Sentry; /// -/// Measures the time it takes to run a given code block and emits this as a metric. The class is -/// designed to be used in a using statement. +/// Measures the time it takes to run a given code block and emits this as a metric. /// /// /// using (var timing = new Timing("my-operation")) @@ -13,9 +12,11 @@ namespace Sentry; /// ... /// } /// -public class Timing: IDisposable +internal class Timing : IDisposable { - private readonly IHub _hub; + private readonly IMetricHub _metricHub; + private readonly SentryOptions _options; + private readonly MetricAggregator _metricAggregator; private readonly string _key; private readonly MeasurementUnit.Duration _unit; private readonly IDictionary? _tags; @@ -26,45 +27,26 @@ public class Timing: IDisposable /// /// Creates a new instance. /// - public Timing(string key, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, - IDictionary? tags = null) - : this(SentrySdk.CurrentHub, key, unit, tags, stackLevel: 2 /* one for each constructor */) + internal Timing(MetricAggregator metricAggregator, IMetricHub metricHub, SentryOptions options, + string key, MeasurementUnit.Duration unit, IDictionary? tags, int stackLevel) { - } - - /// - /// Creates a new instance. - /// - public Timing(IHub hub, string key, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second, - IDictionary? tags = null) - : this(hub, key, unit, tags, stackLevel: 2 /* one for each constructor */) - { - } - - internal Timing(IHub hub, string key, MeasurementUnit.Duration unit, IDictionary? tags, - int stackLevel) - { - _hub = hub; + _metricHub = metricHub; + _options = options; + _metricAggregator = metricAggregator; _key = key; _unit = unit; _tags = tags; _stopwatch.Start(); - ITransactionTracer? currentTransaction = null; - hub.ConfigureScope(s => currentTransaction = s.Transaction); - _span = currentTransaction is {} transaction - ? transaction.StartChild("metric.timing", key) - : hub.StartTransaction("metric.timing", key); + + _span = metricHub.StartSpan("metric.timing", key); if (tags is not null) { _span.SetTags(tags); } // Report code locations here for better accuracy - if (hub.Metrics is MetricAggregator metrics) - { - metrics.RecordCodeLocation(MetricType.Distribution, key, unit, stackLevel + 1, _startTime); - } + _metricAggregator.RecordCodeLocation(MetricType.Distribution, key, unit, stackLevel + 1, _startTime); } /// @@ -86,11 +68,11 @@ public void Dispose() MeasurementUnit.Duration.Nanosecond => _stopwatch.Elapsed.TotalMilliseconds * 1000000, _ => throw new ArgumentOutOfRangeException(nameof(_unit), _unit, null) }; - _hub.Metrics.Timing(_key, value, _unit, _tags, _startTime); + _metricAggregator.Timing(_key, value, _unit, _tags, _startTime); } catch (Exception e) { - _hub.GetSentryOptions()?.LogError(e, "Error capturing timing '{0}'", _key); + _options.LogError(e, "Error capturing timing '{0}'", _key); } finally { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt index 5aeb3cded7..c8f0fc1543 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt @@ -224,6 +224,7 @@ namespace Sentry public interface IHub : Sentry.ISentryClient, Sentry.ISentryScopeManager { Sentry.SentryId LastEventId { get; } + Sentry.IMetricAggregator 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.Hint? hint, System.Action configureScope); @@ -249,6 +250,7 @@ namespace Sentry void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); void Set(string key, int value, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); + System.IDisposable StartTimer(string key, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, int stackLevel = 1); void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); } public interface IScopeObserver @@ -262,7 +264,7 @@ namespace Sentry public interface ISentryClient { bool IsEnabled { get; } - Sentry.IMetricAggregator Metrics { get; } + bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.Hint? hint = null); void CaptureSession(Sentry.SessionUpdate sessionUpdate); void CaptureTransaction(Sentry.SentryTransaction transaction); @@ -490,7 +492,7 @@ namespace Sentry { public SentryClient(Sentry.SentryOptions options) { } public bool IsEnabled { get; } - public Sentry.IMetricAggregator Metrics { get; } + public bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent? @event, Sentry.Scope? scope = null, Sentry.Hint? hint = null) { } public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } @@ -728,6 +730,7 @@ namespace Sentry public static void AddBreadcrumb(Sentry.Infrastructure.ISystemClock? clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public static void BindClient(Sentry.ISentryClient client) { } public static void BindException(System.Exception exception, Sentry.ISpan span) { } + public static bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } public static Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope) { } public static Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Hint? hint, System.Action configureScope) { } public static Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.Hint? hint = null) { } @@ -1063,12 +1066,6 @@ namespace Sentry public override string ToString() { } public static Sentry.SubstringOrRegexPattern op_Implicit(string substringOrRegexPattern) { } } - public class Timing : System.IDisposable - { - public Timing(string key, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null) { } - public Timing(Sentry.IHub hub, string key, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null) { } - public void Dispose() { } - } public class TransactionContext : Sentry.SpanContext, Sentry.ITransactionContext, Sentry.Protocol.ITraceContext { public TransactionContext(string name, string operation, Sentry.SpanId? spanId = default, Sentry.SpanId? parentSpanId = default, Sentry.SentryId? traceId = default, string? description = "", Sentry.SpanStatus? status = default, bool? isSampled = default, bool? isParentSampled = default, Sentry.TransactionNameSource nameSource = 0) { } @@ -1211,6 +1208,7 @@ namespace Sentry.Extensibility public Sentry.IMetricAggregator Metrics { get; } public void BindClient(Sentry.ISentryClient client) { } public void BindException(System.Exception exception, Sentry.ISpan span) { } + public bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Hint? hint, System.Action configureScope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.Hint? hint = null) { } @@ -1251,6 +1249,7 @@ namespace Sentry.Extensibility 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) { } public void BindException(System.Exception exception, Sentry.ISpan span) { } + public bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt index 5aeb3cded7..c8f0fc1543 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt @@ -224,6 +224,7 @@ namespace Sentry public interface IHub : Sentry.ISentryClient, Sentry.ISentryScopeManager { Sentry.SentryId LastEventId { get; } + Sentry.IMetricAggregator 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.Hint? hint, System.Action configureScope); @@ -249,6 +250,7 @@ namespace Sentry void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); void Set(string key, int value, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); + System.IDisposable StartTimer(string key, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, int stackLevel = 1); void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); } public interface IScopeObserver @@ -262,7 +264,7 @@ namespace Sentry public interface ISentryClient { bool IsEnabled { get; } - Sentry.IMetricAggregator Metrics { get; } + bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.Hint? hint = null); void CaptureSession(Sentry.SessionUpdate sessionUpdate); void CaptureTransaction(Sentry.SentryTransaction transaction); @@ -490,7 +492,7 @@ namespace Sentry { public SentryClient(Sentry.SentryOptions options) { } public bool IsEnabled { get; } - public Sentry.IMetricAggregator Metrics { get; } + public bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent? @event, Sentry.Scope? scope = null, Sentry.Hint? hint = null) { } public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } @@ -728,6 +730,7 @@ namespace Sentry public static void AddBreadcrumb(Sentry.Infrastructure.ISystemClock? clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public static void BindClient(Sentry.ISentryClient client) { } public static void BindException(System.Exception exception, Sentry.ISpan span) { } + public static bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } public static Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope) { } public static Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Hint? hint, System.Action configureScope) { } public static Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.Hint? hint = null) { } @@ -1063,12 +1066,6 @@ namespace Sentry public override string ToString() { } public static Sentry.SubstringOrRegexPattern op_Implicit(string substringOrRegexPattern) { } } - public class Timing : System.IDisposable - { - public Timing(string key, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null) { } - public Timing(Sentry.IHub hub, string key, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null) { } - public void Dispose() { } - } public class TransactionContext : Sentry.SpanContext, Sentry.ITransactionContext, Sentry.Protocol.ITraceContext { public TransactionContext(string name, string operation, Sentry.SpanId? spanId = default, Sentry.SpanId? parentSpanId = default, Sentry.SentryId? traceId = default, string? description = "", Sentry.SpanStatus? status = default, bool? isSampled = default, bool? isParentSampled = default, Sentry.TransactionNameSource nameSource = 0) { } @@ -1211,6 +1208,7 @@ namespace Sentry.Extensibility public Sentry.IMetricAggregator Metrics { get; } public void BindClient(Sentry.ISentryClient client) { } public void BindException(System.Exception exception, Sentry.ISpan span) { } + public bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Hint? hint, System.Action configureScope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.Hint? hint = null) { } @@ -1251,6 +1249,7 @@ namespace Sentry.Extensibility 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) { } public void BindException(System.Exception exception, Sentry.ISpan span) { } + public bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 78ff7adc7b..007d2fa570 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -225,6 +225,7 @@ namespace Sentry public interface IHub : Sentry.ISentryClient, Sentry.ISentryScopeManager { Sentry.SentryId LastEventId { get; } + Sentry.IMetricAggregator 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.Hint? hint, System.Action configureScope); @@ -250,6 +251,7 @@ namespace Sentry void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); void Set(string key, int value, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); + System.IDisposable StartTimer(string key, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, int stackLevel = 1); void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); } public interface IScopeObserver @@ -263,7 +265,7 @@ namespace Sentry public interface ISentryClient { bool IsEnabled { get; } - Sentry.IMetricAggregator Metrics { get; } + bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.Hint? hint = null); void CaptureSession(Sentry.SessionUpdate sessionUpdate); void CaptureTransaction(Sentry.SentryTransaction transaction); @@ -491,7 +493,7 @@ namespace Sentry { public SentryClient(Sentry.SentryOptions options) { } public bool IsEnabled { get; } - public Sentry.IMetricAggregator Metrics { get; } + public bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent? @event, Sentry.Scope? scope = null, Sentry.Hint? hint = null) { } public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } @@ -729,6 +731,7 @@ namespace Sentry public static void AddBreadcrumb(Sentry.Infrastructure.ISystemClock? clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public static void BindClient(Sentry.ISentryClient client) { } public static void BindException(System.Exception exception, Sentry.ISpan span) { } + public static bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } public static Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope) { } public static Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Hint? hint, System.Action configureScope) { } public static Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.Hint? hint = null) { } @@ -1064,12 +1067,6 @@ namespace Sentry public override string ToString() { } public static Sentry.SubstringOrRegexPattern op_Implicit(string substringOrRegexPattern) { } } - public class Timing : System.IDisposable - { - public Timing(string key, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null) { } - public Timing(Sentry.IHub hub, string key, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null) { } - public void Dispose() { } - } public class TransactionContext : Sentry.SpanContext, Sentry.ITransactionContext, Sentry.Protocol.ITraceContext { public TransactionContext(string name, string operation, Sentry.SpanId? spanId = default, Sentry.SpanId? parentSpanId = default, Sentry.SentryId? traceId = default, string? description = "", Sentry.SpanStatus? status = default, bool? isSampled = default, bool? isParentSampled = default, Sentry.TransactionNameSource nameSource = 0) { } @@ -1212,6 +1209,7 @@ namespace Sentry.Extensibility public Sentry.IMetricAggregator Metrics { get; } public void BindClient(Sentry.ISentryClient client) { } public void BindException(System.Exception exception, Sentry.ISpan span) { } + public bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Hint? hint, System.Action configureScope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.Hint? hint = null) { } @@ -1252,6 +1250,7 @@ namespace Sentry.Extensibility 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) { } public void BindException(System.Exception exception, Sentry.ISpan span) { } + public bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 83e7cfd213..a7ca1d79c8 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -223,6 +223,7 @@ namespace Sentry public interface IHub : Sentry.ISentryClient, Sentry.ISentryScopeManager { Sentry.SentryId LastEventId { get; } + Sentry.IMetricAggregator 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.Hint? hint, System.Action configureScope); @@ -248,6 +249,7 @@ namespace Sentry void Gauge(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); void Increment(string key, double value = 1, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); void Set(string key, int value, Sentry.MeasurementUnit? unit = default, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); + System.IDisposable StartTimer(string key, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, int stackLevel = 1); void Timing(string key, double value, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null, System.DateTimeOffset? timestamp = default, int stackLevel = 1); } public interface IScopeObserver @@ -261,7 +263,7 @@ namespace Sentry public interface ISentryClient { bool IsEnabled { get; } - Sentry.IMetricAggregator Metrics { get; } + bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope); Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.Hint? hint = null); void CaptureSession(Sentry.SessionUpdate sessionUpdate); void CaptureTransaction(Sentry.SentryTransaction transaction); @@ -489,7 +491,7 @@ namespace Sentry { public SentryClient(Sentry.SentryOptions options) { } public bool IsEnabled { get; } - public Sentry.IMetricAggregator Metrics { get; } + public bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent? @event, Sentry.Scope? scope = null, Sentry.Hint? hint = null) { } public void CaptureSession(Sentry.SessionUpdate sessionUpdate) { } public void CaptureTransaction(Sentry.SentryTransaction transaction) { } @@ -725,6 +727,7 @@ namespace Sentry public static void AddBreadcrumb(Sentry.Infrastructure.ISystemClock? clock, string message, string? category = null, string? type = null, System.Collections.Generic.IDictionary? data = null, Sentry.BreadcrumbLevel level = 0) { } public static void BindClient(Sentry.ISentryClient client) { } public static void BindException(System.Exception exception, Sentry.ISpan span) { } + public static bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } public static Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope) { } public static Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Hint? hint, System.Action configureScope) { } public static Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.Hint? hint = null) { } @@ -1060,12 +1063,6 @@ namespace Sentry public override string ToString() { } public static Sentry.SubstringOrRegexPattern op_Implicit(string substringOrRegexPattern) { } } - public class Timing : System.IDisposable - { - public Timing(string key, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null) { } - public Timing(Sentry.IHub hub, string key, Sentry.MeasurementUnit.Duration unit = 3, System.Collections.Generic.IDictionary? tags = null) { } - public void Dispose() { } - } public class TransactionContext : Sentry.SpanContext, Sentry.ITransactionContext, Sentry.Protocol.ITraceContext { public TransactionContext(string name, string operation, Sentry.SpanId? spanId = default, Sentry.SpanId? parentSpanId = default, Sentry.SentryId? traceId = default, string? description = "", Sentry.SpanStatus? status = default, bool? isSampled = default, bool? isParentSampled = default, Sentry.TransactionNameSource nameSource = 0) { } @@ -1208,6 +1205,7 @@ namespace Sentry.Extensibility public Sentry.IMetricAggregator Metrics { get; } public void BindClient(Sentry.ISentryClient client) { } public void BindException(System.Exception exception, Sentry.ISpan span) { } + public bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Hint? hint, System.Action configureScope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope = null, Sentry.Hint? hint = null) { } @@ -1248,6 +1246,7 @@ namespace Sentry.Extensibility 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) { } public void BindException(System.Exception exception, Sentry.ISpan span) { } + public bool CaptureEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, Sentry.Scope? scope) { } public Sentry.SentryId CaptureEvent(Sentry.SentryEvent evt, System.Action configureScope) { } diff --git a/test/Sentry.Tests/MetricAggregatorTests.cs b/test/Sentry.Tests/MetricAggregatorTests.cs index 04d0daeeee..cbb68bad33 100644 --- a/test/Sentry.Tests/MetricAggregatorTests.cs +++ b/test/Sentry.Tests/MetricAggregatorTests.cs @@ -7,12 +7,12 @@ public class MetricAggregatorTests class Fixture { public SentryOptions Options { get; set; } = new(); - public Action> CaptureMetrics { get; set; } = (_ => { }); - public Action CaptureCodeLocations { get; set; } = (_ => { }); + public IHub Hub { get; set; } = Substitute.For(); + public IMetricHub MetricHub { get; set; } = Substitute.For(); public bool DisableFlushLoop { get; set; } = true; public TimeSpan? FlushInterval { get; set; } public MetricAggregator GetSut() - => new(Options, CaptureMetrics, CaptureCodeLocations, disableLoopTask: DisableFlushLoop, flushInterval: FlushInterval); + => new(Options, MetricHub, disableLoopTask: DisableFlushLoop, flushInterval: FlushInterval); } // private readonly Fixture _fixture = new(); @@ -177,13 +177,14 @@ public async Task GetFlushableBuckets_IsThreadsafe() MetricHelper.FlushShift = 0.0; _fixture.DisableFlushLoop = false; _fixture.FlushInterval = TimeSpan.FromMilliseconds(100); - _fixture.CaptureMetrics = metrics => - { - foreach (var metric in metrics) + _fixture.MetricHub.CaptureMetrics(Arg.Do>(metrics => { - Interlocked.Add(ref sent, (int)((CounterMetric)metric).Value); + foreach (var metric in metrics) + { + Interlocked.Add(ref sent, (int)((CounterMetric)metric).Value); + } } - }; + )); var sut = _fixture.GetSut(); // Act... spawn some threads that add loads of metrics From 467676e38d55bb3aea8d00be98330576d12d3f88 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 30 Jan 2024 16:53:38 +1300 Subject: [PATCH 2/2] Capture built in metrics from System.Diagnostics.Metrics API (#3052) --- CHANGELOG.md | 4 + .../Sentry.Samples.Console.Metrics/Program.cs | 84 +++-- src/Sentry/BuiltInSystemDiagnosticsMeters.cs | 173 ++++++++++ src/Sentry/ExperimentalMetricsOptions.cs | 59 ++++ .../SystemDiagnosticsMetricsIntegration.cs | 38 +++ .../SystemDiagnosticsMetricsListener.cs | 87 +++++ src/Sentry/SentryOptions.cs | 30 +- src/Sentry/SentryOptionsExtensions.cs | 9 + src/Sentry/SubstringOrRegexPattern.cs | 11 +- ...piApprovalTests.Run.DotNet6_0.verified.txt | 18 ++ ...piApprovalTests.Run.DotNet7_0.verified.txt | 18 ++ ...piApprovalTests.Run.DotNet8_0.verified.txt | 19 ++ .../ApiApprovalTests.Run.Net4_8.verified.txt | 18 ++ .../BuiltInSystemDiagnosticsMetersTests.cs | 104 ++++++ ...ystemDiagnosticsMetricsIntegrationTests.cs | 79 +++++ .../SystemDiagnosticsMetricsListenerTests.cs | 300 ++++++++++++++++++ .../SentryOptionsExtensionsTests.cs | 10 + ...re_properly_registered.DotNet.verified.txt | 10 + ...y_registered.DotNet6_0.DotNet.verified.txt | 38 +++ ...gistered.DotNet6_0.OSX.DotNet.verified.txt | 38 +++ ...ered.DotNet6_0.Windows.DotNet.verified.txt | 44 +++ ...y_registered.DotNet7_0.DotNet.verified.txt | 38 +++ ...gistered.DotNet7_0.OSX.DotNet.verified.txt | 38 +++ ...ered.DotNet7_0.Windows.DotNet.verified.txt | 44 +++ ...y_registered.DotNet8_0.DotNet.verified.txt | 48 +++ ...gistered.DotNet8_0.OSX.DotNet.verified.txt | 48 +++ ...ered.DotNet8_0.Windows.DotNet.verified.txt | 54 ++++ ...operly_registered.Net4_8.Mono.verified.txt | 38 +++ ...roperly_registered.Net4_8.Net.verified.txt | 38 +++ ...ly_registered.Net4_8.OSX.Mono.verified.txt | 38 +++ ...registered.Net4_8.Windows.Net.verified.txt | 38 +++ .../Sentry.Tests/SentryOptionsTests.verify.cs | 7 +- .../SubstringOrRegexPatternTests.cs | 16 + 33 files changed, 1594 insertions(+), 42 deletions(-) create mode 100644 src/Sentry/BuiltInSystemDiagnosticsMeters.cs create mode 100644 src/Sentry/ExperimentalMetricsOptions.cs create mode 100644 src/Sentry/Integrations/SystemDiagnosticsMetricsIntegration.cs create mode 100644 src/Sentry/Internal/SystemDiagnosticsMetricsListener.cs create mode 100644 test/Sentry.Tests/BuiltInSystemDiagnosticsMetersTests.cs create mode 100644 test/Sentry.Tests/Integrations/SystemDiagnosticsMetricsIntegrationTests.cs create mode 100644 test/Sentry.Tests/Internals/SystemDiagnosticsMetricsListenerTests.cs create mode 100644 test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet6_0.DotNet.verified.txt create mode 100644 test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet6_0.OSX.DotNet.verified.txt create mode 100644 test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet6_0.Windows.DotNet.verified.txt create mode 100644 test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet7_0.DotNet.verified.txt create mode 100644 test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet7_0.OSX.DotNet.verified.txt create mode 100644 test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet7_0.Windows.DotNet.verified.txt create mode 100644 test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.DotNet.verified.txt create mode 100644 test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.OSX.DotNet.verified.txt create mode 100644 test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.Windows.DotNet.verified.txt create mode 100644 test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.Net4_8.Mono.verified.txt create mode 100644 test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.Net4_8.Net.verified.txt create mode 100644 test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.Net4_8.OSX.Mono.verified.txt create mode 100644 test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.Net4_8.Windows.Net.verified.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index f5a9089dd4..2e7971e57c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Added support for capturing built in metrics from the System.Diagnostics.Metrics API ([#3052](https://github.com/getsentry/sentry-dotnet/pull/3052)) + ### Significant change in behavior - Added `Sentry` namespace to global usings when `ImplicitUsings` is enabled ([#3043](https://github.com/getsentry/sentry-dotnet/pull/3043)) diff --git a/samples/Sentry.Samples.Console.Metrics/Program.cs b/samples/Sentry.Samples.Console.Metrics/Program.cs index 7854b6e39a..1cff970c34 100644 --- a/samples/Sentry.Samples.Console.Metrics/Program.cs +++ b/samples/Sentry.Samples.Console.Metrics/Program.cs @@ -1,10 +1,20 @@ -namespace Sentry.Samples.Console.Metrics; +using System.Diagnostics.Metrics; +using System.Text.RegularExpressions; + +namespace Sentry.Samples.Console.Metrics; internal static class Program { - private static readonly Random Roll = new(); + private static readonly Random Roll = Random.Shared; + + // Sentry also supports capturing System.Diagnostics.Metrics + private static readonly Meter HatsMeter = new("HatCo.HatStore", "1.0.0"); + private static readonly Counter HatsSold = HatsMeter.CreateCounter( + name: "hats-sold", + unit: "Hats", + description: "The number of hats sold in our store"); - private static void Main() + private static async Task Main() { // Enable the SDK using (SentrySdk.Init(options => @@ -20,41 +30,44 @@ private static void Main() // Initialize some (non null) ExperimentalMetricsOptions to enable Sentry Metrics, options.ExperimentalMetrics = new ExperimentalMetricsOptions { - EnableCodeLocations = - true // Set this to false if you don't want to track code locations for some reason + EnableCodeLocations = true, // Set this to false if you don't want to track code locations + CaptureSystemDiagnosticsInstruments = [ + // Capture System.Diagnostics.Metrics matching the name "HatCo.HatStore", which is the name + // of the custom HatsMeter defined above + "hats-sold" + ], + // Capture all built in metrics (this is the default - you can override this to capture some or + // none of these if you prefer) + CaptureSystemDiagnosticsMeters = BuiltInSystemDiagnosticsMeters.All }; })) { System.Console.WriteLine("Measure, Yeah, Measure!"); - Action[] actions = - [ - () => PlaySetBingo(10), - () => CreateRevenueGauge(100), - () => MeasureShrimp(30), - ]; - while (true) + + Action[] actions = [PlaySetBingo, CreateRevenueGauge, MeasureShrimp, SellHats]; + do { - // Perform your task here - var actionIdx = Roll.Next(0, actions.Length); - actions[actionIdx](); + // Run a random action + var idx = Roll.Next(0, actions.Length); + actions[idx](); + + // Make an API call + await CallSampleApiAsync(); // Optional: Delay to prevent tight looping - var sleepTime = Roll.Next(1, 10); + var sleepTime = Roll.Next(1, 5); System.Console.WriteLine($"Sleeping for {sleepTime} second(s)."); System.Console.WriteLine("Press any key to stop..."); Thread.Sleep(TimeSpan.FromSeconds(sleepTime)); - // Check if a key has been pressed - if (System.Console.KeyAvailable) - { - break; - } } + while (!System.Console.KeyAvailable); System.Console.WriteLine("Measure up"); } } - private static void PlaySetBingo(int attempts) + private static void PlaySetBingo() { + const int attempts = 10; var solution = new[] { 3, 5, 7, 11, 13, 17 }; // StartTimer creates a distribution that is designed to measure the amount of time it takes to run code @@ -74,8 +87,9 @@ private static void PlaySetBingo(int attempts) } } - private static void CreateRevenueGauge(int sampleCount) + private static void CreateRevenueGauge() { + const int sampleCount = 100; using (SentrySdk.Metrics.StartTimer(nameof(CreateRevenueGauge), MeasurementUnit.Duration.Millisecond)) { for (var i = 0; i < sampleCount; i++) @@ -88,8 +102,9 @@ private static void CreateRevenueGauge(int sampleCount) } } - private static void MeasureShrimp(int sampleCount) + private static void MeasureShrimp() { + const int sampleCount = 30; using (SentrySdk.Metrics.StartTimer(nameof(MeasureShrimp), MeasurementUnit.Duration.Millisecond)) { for (var i = 0; i < sampleCount; i++) @@ -100,4 +115,25 @@ private static void MeasureShrimp(int sampleCount) } } } + + private static void SellHats() + { + // Here we're emitting the metric using System.Diagnostics.Metrics instead of SentrySdk.Metrics. + // We won't see accurate code locations for these, so Sentry.Metrics are preferable but support + // for System.Diagnostics.Metrics means Sentry can collect a bunch built in metrics without you + // having to instrument anything... see case 4 below + HatsSold.Add(Roll.Next(0, 1000)); + } + + private static async Task CallSampleApiAsync() + { + // Here we demonstrate collecting some built in metrics for HTTP requests... this works because + // we've configured ExperimentalMetricsOptions.CaptureInstruments to match "http.client.*" + // + // See https://learn.microsoft.com/en-us/dotnet/core/diagnostics/built-in-metrics-system-net#systemnethttp + var httpClient = new HttpClient(); + var url = "https://api.sampleapis.com/coffee/hot"; + var result = await httpClient.GetAsync(url); + System.Console.WriteLine($"GET {url} {result.StatusCode}"); + } } diff --git a/src/Sentry/BuiltInSystemDiagnosticsMeters.cs b/src/Sentry/BuiltInSystemDiagnosticsMeters.cs new file mode 100644 index 0000000000..be846b67d1 --- /dev/null +++ b/src/Sentry/BuiltInSystemDiagnosticsMeters.cs @@ -0,0 +1,173 @@ +namespace Sentry; + +/// +/// Well known values for built in metrics that can be configured for +/// +/// +public static partial class BuiltInSystemDiagnosticsMeters +{ + private const string MicrosoftAspNetCoreHostingPattern = @"^Microsoft\.AspNetCore\.Hosting$"; + private const string MicrosoftAspNetCoreRoutingPattern = @"^Microsoft\.AspNetCore\.Routing$"; + private const string MicrosoftAspNetCoreDiagnosticsPattern = @"^Microsoft\.AspNetCore\.Diagnostics$"; + private const string MicrosoftAspNetCoreRateLimitingPattern = @"^Microsoft\.AspNetCore\.RateLimiting$"; + private const string MicrosoftAspNetCoreHeaderParsingPattern = @"^Microsoft\.AspNetCore\.HeaderParsing$"; + private const string MicrosoftAspNetCoreServerKestrelPattern = @"^Microsoft\.AspNetCore\.Server\.Kestrel$"; + private const string MicrosoftAspNetCoreHttpConnectionsPattern = @"^Microsoft\.AspNetCore\.Http\.Connections$"; + private const string MicrosoftExtensionsDiagnosticsHealthChecksPattern = @"^Microsoft\.Extensions\.Diagnostics\.HealthChecks$"; + private const string MicrosoftExtensionsDiagnosticsResourceMonitoringPattern = @"^Microsoft\.Extensions\.Diagnostics\.ResourceMonitoring$"; + private const string SystemNetNameResolutionPattern = @"^System\.Net\.NameResolution$"; + private const string SystemNetHttpPattern = @"^System\.Net\.Http$"; + + /// + /// Matches the built in Microsoft.AspNetCore.Hosting metrics + /// +#if NET8_0_OR_GREATER + public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreHosting = MicrosoftAspNetCoreHostingRegex(); + + [GeneratedRegex(MicrosoftAspNetCoreHostingPattern, RegexOptions.Compiled)] + private static partial Regex MicrosoftAspNetCoreHostingRegex(); +#else + public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreHosting = new Regex(MicrosoftAspNetCoreHostingPattern, RegexOptions.Compiled); +#endif + + /// + /// Matches the built in Microsoft.AspNetCore.Routing metrics + /// +#if NET8_0_OR_GREATER + public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreRouting = MicrosoftAspNetCoreRoutingRegex(); + + [GeneratedRegex(MicrosoftAspNetCoreRoutingPattern, RegexOptions.Compiled)] + private static partial Regex MicrosoftAspNetCoreRoutingRegex(); +#else + public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreRouting = new Regex(MicrosoftAspNetCoreRoutingPattern, RegexOptions.Compiled); +#endif + + /// + /// Matches the built in Microsoft.AspNetCore.Diagnostics metrics + /// +#if NET8_0_OR_GREATER + public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreDiagnostics = MicrosoftAspNetCoreDiagnosticsRegex(); + + [GeneratedRegex(MicrosoftAspNetCoreDiagnosticsPattern, RegexOptions.Compiled)] + private static partial Regex MicrosoftAspNetCoreDiagnosticsRegex(); +#else + public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreDiagnostics = new Regex(MicrosoftAspNetCoreDiagnosticsPattern, RegexOptions.Compiled); +#endif + + /// + /// Matches the built in Microsoft.AspNetCore.RateLimiting metrics + /// +#if NET8_0_OR_GREATER + public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreRateLimiting = MicrosoftAspNetCoreRateLimitingRegex(); + + [GeneratedRegex(MicrosoftAspNetCoreRateLimitingPattern, RegexOptions.Compiled)] + private static partial Regex MicrosoftAspNetCoreRateLimitingRegex(); +#else + public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreRateLimiting = new Regex(MicrosoftAspNetCoreRateLimitingPattern, RegexOptions.Compiled); +#endif + + /// + /// Matches the built in Microsoft.AspNetCore.HeaderParsing metrics + /// +#if NET8_0_OR_GREATER + public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreHeaderParsing = MicrosoftAspNetCoreHeaderParsingRegex(); + + [GeneratedRegex(MicrosoftAspNetCoreHeaderParsingPattern, RegexOptions.Compiled)] + private static partial Regex MicrosoftAspNetCoreHeaderParsingRegex(); +#else + public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreHeaderParsing = new Regex(MicrosoftAspNetCoreHeaderParsingPattern, RegexOptions.Compiled); +#endif + + /// + /// Matches the built in Microsoft.AspNetCore.Server.Kestrel metrics + /// +#if NET8_0_OR_GREATER + public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreServerKestrel = MicrosoftAspNetCoreServerKestrelRegex(); + + [GeneratedRegex(MicrosoftAspNetCoreServerKestrelPattern, RegexOptions.Compiled)] + private static partial Regex MicrosoftAspNetCoreServerKestrelRegex(); +#else + public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreServerKestrel = new Regex(MicrosoftAspNetCoreServerKestrelPattern, RegexOptions.Compiled); +#endif + + /// + /// Matches the built in Microsoft.AspNetCore.Http.Connections metrics + /// +#if NET8_0_OR_GREATER + public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreHttpConnections = MicrosoftAspNetCoreHttpConnectionsRegex(); + + [GeneratedRegex(MicrosoftAspNetCoreHttpConnectionsPattern, RegexOptions.Compiled)] + private static partial Regex MicrosoftAspNetCoreHttpConnectionsRegex(); +#else + public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreHttpConnections = new Regex(MicrosoftAspNetCoreHttpConnectionsPattern, RegexOptions.Compiled); +#endif + + /// + /// Matches the built in Microsoft.Extensions.Diagnostics.HealthChecks metrics + /// +#if NET8_0_OR_GREATER + public static readonly SubstringOrRegexPattern MicrosoftExtensionsDiagnosticsHealthChecks = MicrosoftExtensionsDiagnosticsHealthChecksRegex(); + + [GeneratedRegex(MicrosoftExtensionsDiagnosticsHealthChecksPattern, RegexOptions.Compiled)] + private static partial Regex MicrosoftExtensionsDiagnosticsHealthChecksRegex(); +#else + public static readonly SubstringOrRegexPattern MicrosoftExtensionsDiagnosticsHealthChecks = new Regex(MicrosoftExtensionsDiagnosticsHealthChecksPattern, RegexOptions.Compiled); +#endif + + /// + /// Matches the built in Microsoft.Extensions.Diagnostics.ResourceMonitoring metrics + /// +#if NET8_0_OR_GREATER + public static readonly SubstringOrRegexPattern MicrosoftExtensionsDiagnosticsResourceMonitoring = MicrosoftExtensionsDiagnosticsResourceMonitoringRegex(); + + [GeneratedRegex(MicrosoftExtensionsDiagnosticsResourceMonitoringPattern, RegexOptions.Compiled)] + private static partial Regex MicrosoftExtensionsDiagnosticsResourceMonitoringRegex(); +#else + public static readonly SubstringOrRegexPattern MicrosoftExtensionsDiagnosticsResourceMonitoring = new Regex(MicrosoftExtensionsDiagnosticsResourceMonitoringPattern, RegexOptions.Compiled); +#endif + + /// + /// Matches the built in System.Net.NameResolution metrics + /// +#if NET8_0_OR_GREATER + public static readonly SubstringOrRegexPattern SystemNetNameResolution = SystemNetNameResolutionRegex(); + + [GeneratedRegex(SystemNetNameResolutionPattern, RegexOptions.Compiled)] + private static partial Regex SystemNetNameResolutionRegex(); +#else + public static readonly SubstringOrRegexPattern SystemNetNameResolution = new Regex(SystemNetNameResolutionPattern, RegexOptions.Compiled); +#endif + + /// + /// Matches the built in metrics + /// +#if NET8_0_OR_GREATER + public static readonly SubstringOrRegexPattern SystemNetHttp = SystemNetHttpRegex(); + + [GeneratedRegex(SystemNetHttpPattern, RegexOptions.Compiled)] + private static partial Regex SystemNetHttpRegex(); +#else + public static readonly SubstringOrRegexPattern SystemNetHttp = new Regex(SystemNetHttpPattern, RegexOptions.Compiled); +#endif + + private static readonly Lazy> LazyAll = new(() => new List + { + MicrosoftAspNetCoreHosting, + MicrosoftAspNetCoreRouting, + MicrosoftAspNetCoreDiagnostics, + MicrosoftAspNetCoreRateLimiting, + MicrosoftAspNetCoreHeaderParsing, + MicrosoftAspNetCoreServerKestrel, + MicrosoftAspNetCoreHttpConnections, + SystemNetNameResolution, + SystemNetHttp, + MicrosoftExtensionsDiagnosticsHealthChecks, + MicrosoftExtensionsDiagnosticsResourceMonitoring + }); + + /// + /// Matches all built in metrics + /// + /// + public static IList All => LazyAll.Value; +} diff --git a/src/Sentry/ExperimentalMetricsOptions.cs b/src/Sentry/ExperimentalMetricsOptions.cs new file mode 100644 index 0000000000..5a71606c36 --- /dev/null +++ b/src/Sentry/ExperimentalMetricsOptions.cs @@ -0,0 +1,59 @@ +namespace Sentry; + +/// +/// Settings for the experimental Metrics feature. This feature is preview only and will very likely change in the future +/// without a major version bump... so use at your own risk. +/// +public class ExperimentalMetricsOptions +{ + /// + /// Determines whether code locations should be recorded for Metrics + /// + public bool EnableCodeLocations { get; set; } = true; + + private IList _captureSystemDiagnosticsInstruments = new List(); + + /// + /// + /// A list of Substrings or Regular Expressions. Any `System.Diagnostics.Metrics.Instrument` whose name + /// matches one of the items in this list will be collected and reported to Sentry. + /// + /// + /// These can be either custom Instruments that you have created or any of the built in metrics that are available. + /// + /// + /// See https://learn.microsoft.com/en-us/dotnet/core/diagnostics/built-in-metrics for more information. + /// + /// + public IList CaptureSystemDiagnosticsInstruments + { + // NOTE: During configuration binding, .NET 6 and lower used to just call Add on the existing item. + // .NET 7 changed this to call the setter with an array that already starts with the old value. + // We have to handle both cases. + get => _captureSystemDiagnosticsInstruments; + set => _captureSystemDiagnosticsInstruments = value.WithConfigBinding(); + } + + private IList _captureSystemDiagnosticsMeters = BuiltInSystemDiagnosticsMeters.All; + + /// + /// + /// A list of Substrings or Regular Expressions. Instruments for any `System.Diagnostics.Metrics.Meter` + /// whose name matches one of the items in this list will be collected and reported to Sentry. + /// + /// + /// These can be either custom Instruments that you have created or any of the built in metrics that are available. + /// + /// + /// See https://learn.microsoft.com/en-us/dotnet/core/diagnostics/built-in-metrics for more information. + /// + /// + public IList CaptureSystemDiagnosticsMeters + { + // NOTE: During configuration binding, .NET 6 and lower used to just call Add on the existing item. + // .NET 7 changed this to call the setter with an array that already starts with the old value. + // We have to handle both cases. + get => _captureSystemDiagnosticsMeters; + set => _captureSystemDiagnosticsMeters = value.WithConfigBinding(); + } +} diff --git a/src/Sentry/Integrations/SystemDiagnosticsMetricsIntegration.cs b/src/Sentry/Integrations/SystemDiagnosticsMetricsIntegration.cs new file mode 100644 index 0000000000..efa1af2755 --- /dev/null +++ b/src/Sentry/Integrations/SystemDiagnosticsMetricsIntegration.cs @@ -0,0 +1,38 @@ +#if NET8_0_OR_GREATER +using Sentry.Extensibility; +using Sentry.Internal; + +namespace Sentry.Integrations; + +internal class SystemDiagnosticsMetricsIntegration : ISdkIntegration +{ + private readonly Action _initializeListener; + internal const string NoListenersAreConfiguredMessage = "System.Diagnostics.Metrics Integration is disabled because no listeners are configured."; + + public SystemDiagnosticsMetricsIntegration() + { + _initializeListener = SystemDiagnosticsMetricsListener.InitializeDefaultListener; + } + + /// + /// Overload for testing purposes + /// + internal SystemDiagnosticsMetricsIntegration(Action initializeListener) + { + _initializeListener = initializeListener; + } + + public void Register(IHub hub, SentryOptions options) + { + var captureInstruments = options.ExperimentalMetrics?.CaptureSystemDiagnosticsInstruments; + var captureMeters = options.ExperimentalMetrics?.CaptureSystemDiagnosticsMeters; + if (captureInstruments is not { Count: > 0 } && captureMeters is not { Count: > 0 }) + { + options.LogInfo(NoListenersAreConfiguredMessage); + return; + } + + _initializeListener(options.ExperimentalMetrics!); + } +} +#endif diff --git a/src/Sentry/Internal/SystemDiagnosticsMetricsListener.cs b/src/Sentry/Internal/SystemDiagnosticsMetricsListener.cs new file mode 100644 index 0000000000..a18f618781 --- /dev/null +++ b/src/Sentry/Internal/SystemDiagnosticsMetricsListener.cs @@ -0,0 +1,87 @@ +#if NET8_0_OR_GREATER +using System.Diagnostics.Metrics; + +namespace Sentry.Internal; + +internal class SystemDiagnosticsMetricsListener : IDisposable +{ + private readonly Lazy _metricsAggregator; + private IMetricAggregator MetricsAggregator => _metricsAggregator.Value; + private static SystemDiagnosticsMetricsListener? DefaultListener; + + internal readonly MeterListener _sentryListener = new (); + + private SystemDiagnosticsMetricsListener(ExperimentalMetricsOptions metricsOptions) + : this(metricsOptions, () => SentrySdk.Metrics) + { + } + + /// + /// Overload for testing purposes - allows us to supply a mock IMetricAggregator + /// + internal SystemDiagnosticsMetricsListener(ExperimentalMetricsOptions metricsOptions, Func metricsAggregatorResolver) + { + _metricsAggregator = new Lazy(metricsAggregatorResolver); + _sentryListener.InstrumentPublished = (instrument, listener) => + { + if (metricsOptions.CaptureSystemDiagnosticsMeters.ContainsMatch(instrument.Meter.Name) + || metricsOptions.CaptureSystemDiagnosticsInstruments.ContainsMatch(instrument.Name)) + { + listener.EnableMeasurementEvents(instrument); + } + }; + _sentryListener.SetMeasurementEventCallback(RecordMeasurement); + _sentryListener.SetMeasurementEventCallback(RecordMeasurement); + _sentryListener.SetMeasurementEventCallback(RecordMeasurement); + _sentryListener.SetMeasurementEventCallback(RecordMeasurement); + _sentryListener.SetMeasurementEventCallback(RecordMeasurement); + _sentryListener.SetMeasurementEventCallback(RecordMeasurement); + _sentryListener.SetMeasurementEventCallback(RecordMeasurement); + _sentryListener.Start(); + } + + internal static void InitializeDefaultListener(ExperimentalMetricsOptions metricsOptions) + { + var oldListener = Interlocked.Exchange( + ref DefaultListener, + new SystemDiagnosticsMetricsListener(metricsOptions) + ); + oldListener?.Dispose(); + } + + internal void RecordMeasurement( + Instrument instrument, + T measurement, + ReadOnlySpan> tags, + object? _) + where T: struct, IConvertible + { + var unit = MeasurementUnit.Parse(instrument.Unit); + var tagDict = tags.ToImmutableArray().ToImmutableDictionary( + kvp => kvp.Key, + kvp => kvp.Value?.ToString() ?? string.Empty + ); + var doubleMeasurement = Convert.ToDouble(measurement); + switch (instrument) + { + case Counter: + case UpDownCounter: + case ObservableCounter: + case ObservableUpDownCounter: + MetricsAggregator.Increment(instrument.Name, doubleMeasurement, unit, tagDict); + break; + case Histogram: + MetricsAggregator.Distribution(instrument.Name, doubleMeasurement, unit, tagDict); + break; + case ObservableGauge: + MetricsAggregator.Gauge(instrument.Name, doubleMeasurement, unit, tagDict); + break; + } + } + + public void Dispose() + { + _sentryListener.Dispose(); + } +} +#endif diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index c85d33e212..d6e7ed8a84 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -194,6 +194,13 @@ internal IEnumerable Integrations } #endif +#if NET8_0_OR_GREATER + if ((_defaultIntegrations & DefaultIntegrations.SystemDiagnosticsMetricsIntegration) != 0) + { + yield return new SystemDiagnosticsMetricsIntegration(); + } +#endif + foreach (var integration in _integrations) { yield return integration; @@ -709,7 +716,7 @@ public IDiagnosticLogger? DiagnosticLogger public IList FailedRequestTargets { get => _failedRequestTargets.Value; - set => _failedRequestTargets = new(value.SetWithConfigBinding); + set => _failedRequestTargets = new(value.WithConfigBinding); } /// @@ -911,7 +918,7 @@ public IList TracePropagationTargets // .NET 7 changed this to call the setter with an array that already starts with the old value. // We have to handle both cases. get => _tracePropagationTargets; - set => _tracePropagationTargets = value.SetWithConfigBinding(); + set => _tracePropagationTargets = value.WithConfigBinding(); } internal ITransactionProfilerFactory? TransactionProfilerFactory { get; set; } @@ -1212,6 +1219,9 @@ public SentryOptions() #endif #if NET5_0_OR_GREATER && !__MOBILE__ | DefaultIntegrations.WinUiUnhandledExceptionIntegration +#endif +#if NET8_0_OR_GREATER + | DefaultIntegrations.SystemDiagnosticsMetricsIntegration #endif ; @@ -1312,19 +1322,9 @@ internal enum DefaultIntegrations #endif #if NET5_0_OR_GREATER && !__MOBILE__ WinUiUnhandledExceptionIntegration = 1 << 6, +#endif +#if NET8_0_OR_GREATER + SystemDiagnosticsMetricsIntegration = 1 << 7, #endif } } - -/// -/// Settings for the experimental Metrics feature. This feature is preview only and will very likely change in the future -/// without a major version bump... so use at your own risk. -/// -public class ExperimentalMetricsOptions -{ - /// - /// Determines the sample rate for metrics. 0.0 means no metrics will be sent (metrics disabled). 1.0 implies all - /// metrics will be sent. - /// - public bool EnableCodeLocations { get; set; } = true; -} diff --git a/src/Sentry/SentryOptionsExtensions.cs b/src/Sentry/SentryOptionsExtensions.cs index 6c88f31637..be67336fcc 100644 --- a/src/Sentry/SentryOptionsExtensions.cs +++ b/src/Sentry/SentryOptionsExtensions.cs @@ -84,6 +84,15 @@ public static void DisableWinUiUnhandledExceptionIntegration(this SentryOptions => options.RemoveDefaultIntegration(SentryOptions.DefaultIntegrations.WinUiUnhandledExceptionIntegration); #endif +#if NET8_0_OR_GREATER + /// + /// Disables the System.Diagnostics.Metrics integration. + /// + /// The SentryOptions to remove the integration from. + public static void DisableSystemDiagnosticsMetricsIntegration(this SentryOptions options) + => options.RemoveDefaultIntegration(SentryOptions.DefaultIntegrations.SystemDiagnosticsMetricsIntegration); +#endif + /// /// Add an integration /// diff --git a/src/Sentry/SubstringOrRegexPattern.cs b/src/Sentry/SubstringOrRegexPattern.cs index 1c585efed5..502b613c23 100644 --- a/src/Sentry/SubstringOrRegexPattern.cs +++ b/src/Sentry/SubstringOrRegexPattern.cs @@ -47,6 +47,15 @@ public static implicit operator SubstringOrRegexPattern(string substringOrRegexP return new SubstringOrRegexPattern(substringOrRegexPattern); } + /// + /// Implicitly converts a to a . + /// + /// + public static implicit operator SubstringOrRegexPattern(Regex regex) + { + return new SubstringOrRegexPattern(regex); + } + /// public override string ToString() => _substring ?? _regex?.ToString() ?? ""; @@ -115,7 +124,7 @@ public static bool ContainsMatch(this IEnumerable targe /// The List Type /// The set of values to be assigned /// A IList of type T that will be consistent even if it has been set via Config - public static IList SetWithConfigBinding(this IList value) + public static IList WithConfigBinding(this IList value) where T: SubstringOrRegexPattern { switch (value.Count) diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt index c8f0fc1543..6953004d5d 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt @@ -49,6 +49,21 @@ namespace Sentry [System.Runtime.Serialization.EnumMember(Value="critical")] Critical = 3, } + public static class BuiltInSystemDiagnosticsMeters + { + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreDiagnostics; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreHeaderParsing; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreHosting; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreHttpConnections; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreRateLimiting; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreRouting; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreServerKestrel; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftExtensionsDiagnosticsHealthChecks; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftExtensionsDiagnosticsResourceMonitoring; + public static readonly Sentry.SubstringOrRegexPattern SystemNetHttp; + public static readonly Sentry.SubstringOrRegexPattern SystemNetNameResolution; + public static System.Collections.Generic.IList All { get; } + } public class ByteAttachmentContent : Sentry.IAttachmentContent { public ByteAttachmentContent(byte[] bytes) { } @@ -129,6 +144,8 @@ namespace Sentry public class ExperimentalMetricsOptions { public ExperimentalMetricsOptions() { } + public System.Collections.Generic.IList CaptureSystemDiagnosticsInstruments { get; set; } + public System.Collections.Generic.IList CaptureSystemDiagnosticsMeters { get; set; } public bool EnableCodeLocations { get; set; } } public class FileAttachmentContent : Sentry.IAttachmentContent @@ -1065,6 +1082,7 @@ namespace Sentry public override int GetHashCode() { } public override string ToString() { } public static Sentry.SubstringOrRegexPattern op_Implicit(string substringOrRegexPattern) { } + public static Sentry.SubstringOrRegexPattern op_Implicit(System.Text.RegularExpressions.Regex regex) { } } public class TransactionContext : Sentry.SpanContext, Sentry.ITransactionContext, Sentry.Protocol.ITraceContext { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt index c8f0fc1543..6953004d5d 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt @@ -49,6 +49,21 @@ namespace Sentry [System.Runtime.Serialization.EnumMember(Value="critical")] Critical = 3, } + public static class BuiltInSystemDiagnosticsMeters + { + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreDiagnostics; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreHeaderParsing; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreHosting; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreHttpConnections; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreRateLimiting; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreRouting; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreServerKestrel; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftExtensionsDiagnosticsHealthChecks; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftExtensionsDiagnosticsResourceMonitoring; + public static readonly Sentry.SubstringOrRegexPattern SystemNetHttp; + public static readonly Sentry.SubstringOrRegexPattern SystemNetNameResolution; + public static System.Collections.Generic.IList All { get; } + } public class ByteAttachmentContent : Sentry.IAttachmentContent { public ByteAttachmentContent(byte[] bytes) { } @@ -129,6 +144,8 @@ namespace Sentry public class ExperimentalMetricsOptions { public ExperimentalMetricsOptions() { } + public System.Collections.Generic.IList CaptureSystemDiagnosticsInstruments { get; set; } + public System.Collections.Generic.IList CaptureSystemDiagnosticsMeters { get; set; } public bool EnableCodeLocations { get; set; } } public class FileAttachmentContent : Sentry.IAttachmentContent @@ -1065,6 +1082,7 @@ namespace Sentry public override int GetHashCode() { } public override string ToString() { } public static Sentry.SubstringOrRegexPattern op_Implicit(string substringOrRegexPattern) { } + public static Sentry.SubstringOrRegexPattern op_Implicit(System.Text.RegularExpressions.Regex regex) { } } public class TransactionContext : Sentry.SpanContext, Sentry.ITransactionContext, Sentry.Protocol.ITraceContext { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 007d2fa570..7c61203e2d 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -49,6 +49,21 @@ namespace Sentry [System.Runtime.Serialization.EnumMember(Value="critical")] Critical = 3, } + public static class BuiltInSystemDiagnosticsMeters + { + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreDiagnostics; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreHeaderParsing; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreHosting; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreHttpConnections; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreRateLimiting; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreRouting; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreServerKestrel; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftExtensionsDiagnosticsHealthChecks; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftExtensionsDiagnosticsResourceMonitoring; + public static readonly Sentry.SubstringOrRegexPattern SystemNetHttp; + public static readonly Sentry.SubstringOrRegexPattern SystemNetNameResolution; + public static System.Collections.Generic.IList All { get; } + } public class ByteAttachmentContent : Sentry.IAttachmentContent { public ByteAttachmentContent(byte[] bytes) { } @@ -130,6 +145,8 @@ namespace Sentry public class ExperimentalMetricsOptions { public ExperimentalMetricsOptions() { } + public System.Collections.Generic.IList CaptureSystemDiagnosticsInstruments { get; set; } + public System.Collections.Generic.IList CaptureSystemDiagnosticsMeters { get; set; } public bool EnableCodeLocations { get; set; } } public class FileAttachmentContent : Sentry.IAttachmentContent @@ -706,6 +723,7 @@ namespace Sentry public static void DisableAppDomainUnhandledExceptionCapture(this Sentry.SentryOptions options) { } public static void DisableDiagnosticSourceIntegration(this Sentry.SentryOptions options) { } public static void DisableDuplicateEventDetection(this Sentry.SentryOptions options) { } + public static void DisableSystemDiagnosticsMetricsIntegration(this Sentry.SentryOptions options) { } public static void DisableUnobservedTaskExceptionCapture(this Sentry.SentryOptions options) { } public static void DisableWinUiUnhandledExceptionIntegration(this Sentry.SentryOptions options) { } public static System.Collections.Generic.IEnumerable GetAllEventProcessors(this Sentry.SentryOptions options) { } @@ -1066,6 +1084,7 @@ namespace Sentry public override int GetHashCode() { } public override string ToString() { } public static Sentry.SubstringOrRegexPattern op_Implicit(string substringOrRegexPattern) { } + public static Sentry.SubstringOrRegexPattern op_Implicit(System.Text.RegularExpressions.Regex regex) { } } public class TransactionContext : Sentry.SpanContext, Sentry.ITransactionContext, Sentry.Protocol.ITraceContext { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index a7ca1d79c8..0dfecbc072 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -49,6 +49,21 @@ namespace Sentry [System.Runtime.Serialization.EnumMember(Value="critical")] Critical = 3, } + public static class BuiltInSystemDiagnosticsMeters + { + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreDiagnostics; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreHeaderParsing; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreHosting; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreHttpConnections; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreRateLimiting; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreRouting; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftAspNetCoreServerKestrel; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftExtensionsDiagnosticsHealthChecks; + public static readonly Sentry.SubstringOrRegexPattern MicrosoftExtensionsDiagnosticsResourceMonitoring; + public static readonly Sentry.SubstringOrRegexPattern SystemNetHttp; + public static readonly Sentry.SubstringOrRegexPattern SystemNetNameResolution; + public static System.Collections.Generic.IList All { get; } + } public class ByteAttachmentContent : Sentry.IAttachmentContent { public ByteAttachmentContent(byte[] bytes) { } @@ -128,6 +143,8 @@ namespace Sentry public class ExperimentalMetricsOptions { public ExperimentalMetricsOptions() { } + public System.Collections.Generic.IList CaptureSystemDiagnosticsInstruments { get; set; } + public System.Collections.Generic.IList CaptureSystemDiagnosticsMeters { get; set; } public bool EnableCodeLocations { get; set; } } public class FileAttachmentContent : Sentry.IAttachmentContent @@ -1062,6 +1079,7 @@ namespace Sentry public override int GetHashCode() { } public override string ToString() { } public static Sentry.SubstringOrRegexPattern op_Implicit(string substringOrRegexPattern) { } + public static Sentry.SubstringOrRegexPattern op_Implicit(System.Text.RegularExpressions.Regex regex) { } } public class TransactionContext : Sentry.SpanContext, Sentry.ITransactionContext, Sentry.Protocol.ITraceContext { diff --git a/test/Sentry.Tests/BuiltInSystemDiagnosticsMetersTests.cs b/test/Sentry.Tests/BuiltInSystemDiagnosticsMetersTests.cs new file mode 100644 index 0000000000..d033049610 --- /dev/null +++ b/test/Sentry.Tests/BuiltInSystemDiagnosticsMetersTests.cs @@ -0,0 +1,104 @@ +namespace Sentry.Tests; + +public class BuiltInSystemDiagnosticsMetersTests +{ + [Fact] + public void MicrosoftAspNetCoreHosting_ExactString_Matches() + { + var pattern = BuiltInSystemDiagnosticsMeters.MicrosoftAspNetCoreHosting; + + pattern.ShouldMatchOnlyExactText("Microsoft.AspNetCore.Hosting"); + } + + [Fact] + public void MicrosoftAspNetCoreRouting_ExactString_Matches() + { + var pattern = BuiltInSystemDiagnosticsMeters.MicrosoftAspNetCoreRouting; + + pattern.ShouldMatchOnlyExactText("Microsoft.AspNetCore.Routing"); + } + + [Fact] + public void MicrosoftAspNetCoreDiagnostics_ExactString_Matches() + { + var pattern = BuiltInSystemDiagnosticsMeters.MicrosoftAspNetCoreDiagnostics; + + pattern.ShouldMatchOnlyExactText("Microsoft.AspNetCore.Diagnostics"); + } + + [Fact] + public void MicrosoftAspNetCoreRateLimiting_ExactString_Matches() + { + var pattern = BuiltInSystemDiagnosticsMeters.MicrosoftAspNetCoreRateLimiting; + + pattern.ShouldMatchOnlyExactText("Microsoft.AspNetCore.RateLimiting"); + } + + [Fact] + public void MicrosoftAspNetCoreHeaderParsing_ExactString_Matches() + { + var pattern = BuiltInSystemDiagnosticsMeters.MicrosoftAspNetCoreHeaderParsing; + + pattern.ShouldMatchOnlyExactText("Microsoft.AspNetCore.HeaderParsing"); + } + + [Fact] + public void MicrosoftAspNetCoreServerKestrel_ExactString_Matches() + { + var pattern = BuiltInSystemDiagnosticsMeters.MicrosoftAspNetCoreServerKestrel; + + pattern.ShouldMatchOnlyExactText("Microsoft.AspNetCore.Server.Kestrel"); + } + + [Fact] + public void MicrosoftAspNetCoreHttpConnections_ExactString_Matches() + { + var pattern = BuiltInSystemDiagnosticsMeters.MicrosoftAspNetCoreHttpConnections; + + pattern.ShouldMatchOnlyExactText("Microsoft.AspNetCore.Http.Connections"); + } + + [Fact] + public void MicrosoftExtensionsDiagnosticsHealthChecks_ExactString_Matches() + { + var pattern = BuiltInSystemDiagnosticsMeters.MicrosoftExtensionsDiagnosticsHealthChecks; + + pattern.ShouldMatchOnlyExactText("Microsoft.Extensions.Diagnostics.HealthChecks"); + } + + [Fact] + public void MicrosoftExtensionsDiagnosticsResourceMonitoring_ExactString_Matches() + { + var pattern = BuiltInSystemDiagnosticsMeters.MicrosoftExtensionsDiagnosticsResourceMonitoring; + + pattern.ShouldMatchOnlyExactText("Microsoft.Extensions.Diagnostics.ResourceMonitoring"); + } + + [Fact] + public void SystemNetNameResolution_ExactString_Matches() + { + var pattern = BuiltInSystemDiagnosticsMeters.SystemNetNameResolution; + + pattern.ShouldMatchOnlyExactText("System.Net.NameResolution"); + } + + [Fact] + public void SystemNetHttp_ExactString_Matches() + { + var pattern = BuiltInSystemDiagnosticsMeters.SystemNetHttp; + + pattern.ShouldMatchOnlyExactText("System.Net.Http"); + } +} + +internal static class BuiltInSystemDiagnosticsMetersTestsExtensions +{ + internal static void ShouldMatchOnlyExactText(this SubstringOrRegexPattern pattern, string actual) + { + var withPrefix = "prefix" + actual; + var withSuffix = actual + "suffix"; + pattern.IsMatch(actual).Should().BeTrue(); + pattern.IsMatch(withPrefix).Should().BeFalse(); + pattern.IsMatch(withSuffix).Should().BeFalse(); + } +} diff --git a/test/Sentry.Tests/Integrations/SystemDiagnosticsMetricsIntegrationTests.cs b/test/Sentry.Tests/Integrations/SystemDiagnosticsMetricsIntegrationTests.cs new file mode 100644 index 0000000000..431afe8a31 --- /dev/null +++ b/test/Sentry.Tests/Integrations/SystemDiagnosticsMetricsIntegrationTests.cs @@ -0,0 +1,79 @@ +#if NET8_0_OR_GREATER +namespace Sentry.Tests.Integrations; + +public class SystemDiagnosticsMetricsIntegrationTests +{ + [Fact] + public void Register_NoListenersConfigured_LogsDisabledMessage() + { + // Arrange + var logger = Substitute.For(); + logger.IsEnabled(Arg.Any()).Returns(true); + var options = new SentryOptions + { + Debug = true, + DiagnosticLogger = logger, + ExperimentalMetrics = new ExperimentalMetricsOptions() + { + CaptureSystemDiagnosticsInstruments = [], + CaptureSystemDiagnosticsMeters = [] + } + }; + var integration = new SystemDiagnosticsMetricsIntegration(); + + // Act + integration.Register(null!, options); + + // Assert + logger.Received(1).Log(SentryLevel.Info, SystemDiagnosticsMetricsIntegration.NoListenersAreConfiguredMessage, null); + } + + [Fact] + public void Register_CaptureSystemDiagnosticsInstruments_Succeeds() + { + // Arrange + var logger = Substitute.For(); + var options = new SentryOptions + { + DiagnosticLogger = logger, + ExperimentalMetrics = new ExperimentalMetricsOptions() + { + CaptureSystemDiagnosticsInstruments = [".*"], + CaptureSystemDiagnosticsMeters = [] + } + }; + var initializeDefaultListener = Substitute.For>(); + var integration = new SystemDiagnosticsMetricsIntegration(initializeDefaultListener); + + // Act + integration.Register(null!, options); + + // Assert + initializeDefaultListener.Received(1)(options.ExperimentalMetrics); + } + + [Fact] + public void Register_CaptureSystemDiagnosticsMeters_Succeeds() + { + // Arrange + var logger = Substitute.For(); + var options = new SentryOptions + { + DiagnosticLogger = logger, + ExperimentalMetrics = new ExperimentalMetricsOptions() + { + CaptureSystemDiagnosticsInstruments = [], + CaptureSystemDiagnosticsMeters = [".*"] + } + }; + var initializeDefaultListener = Substitute.For>(); + var integration = new SystemDiagnosticsMetricsIntegration(initializeDefaultListener); + + // Act + integration.Register(null!, options); + + // Assert + initializeDefaultListener.Received(1)(options.ExperimentalMetrics); + } +} +#endif diff --git a/test/Sentry.Tests/Internals/SystemDiagnosticsMetricsListenerTests.cs b/test/Sentry.Tests/Internals/SystemDiagnosticsMetricsListenerTests.cs new file mode 100644 index 0000000000..59cacb652d --- /dev/null +++ b/test/Sentry.Tests/Internals/SystemDiagnosticsMetricsListenerTests.cs @@ -0,0 +1,300 @@ +#if NET8_0_OR_GREATER +using System.Diagnostics.Metrics; + +namespace Sentry.Tests.Internals; + +public class SystemDiagnosticsMetricsListenerTests +{ + private class Fixture + { + public readonly IMetricAggregator MockAggregator = Substitute.For(); + public readonly ExperimentalMetricsOptions MetricsOptions = new (); + + public SystemDiagnosticsMetricsListener GetSut() + { + return new SystemDiagnosticsMetricsListener(MetricsOptions, () => MockAggregator); + } + } + + private readonly Fixture _fixture = new(); + + private static Meter GetMeter() => new Meter(UniqueName(), "1.0.0"); + private static string UniqueName() => Guid.NewGuid().ToString(); + + [Fact] + public void RecordMeasurement_CounterInstrument_CallsIncrement() + { + // Arrange + var testMeter = GetMeter(); + var instrument = testMeter.CreateCounter(UniqueName(), "unit"); + const int measurement = 2; + ReadOnlySpan> tags = [ + new KeyValuePair("tag1", "value1"), + new KeyValuePair("tag2", 2), + ]; + var expectedTags = tags.ToImmutableArray().ToImmutableDictionary( + kvp => kvp.Key, + kvp => kvp.Value?.ToString() ?? string.Empty + ); + + // Act + var sut = _fixture.GetSut(); + sut.RecordMeasurement(instrument, measurement, tags, null); + + // Assert + _fixture.MockAggregator.Received().Increment( + instrument.Name, + measurement, + MeasurementUnit.Custom(instrument.Unit!), + Arg.Is>(arg => + expectedTags.All(tag => arg.ContainsKey(tag.Key) && arg[tag.Key] == tag.Value) + ) + ); + } + + [Fact] + public void RecordMeasurement_UpDownCounterInstrument_CallsIncrement() + { + // Arrange + var testMeter = GetMeter(); + var instrument = testMeter.CreateUpDownCounter(UniqueName(), "unit"); + const int measurement = -2; + ReadOnlySpan> tags = [ + new KeyValuePair("tag1", "value1"), + new KeyValuePair("tag2", 2), + ]; + var expectedTags = tags.ToImmutableArray().ToImmutableDictionary( + kvp => kvp.Key, + kvp => kvp.Value?.ToString() ?? string.Empty + ); + + // Act + var sut = _fixture.GetSut(); + sut.RecordMeasurement(instrument, measurement, tags, null); + + // Assert + _fixture.MockAggregator.Received().Increment( + instrument.Name, + measurement, + MeasurementUnit.Custom(instrument.Unit!), + Arg.Is>(arg => + expectedTags.All(tag => arg.ContainsKey(tag.Key) && arg[tag.Key] == tag.Value) + ) + ); + } + + [Fact] + public void RecordMeasurement_HistogramInstrument_CallsDistribution() + { + // Arrange + var testMeter = GetMeter(); + var instrument = testMeter.CreateHistogram(UniqueName(), "unit"); + const int measurement = 2; + ReadOnlySpan> tags = [ + new KeyValuePair("tag1", "value1"), + new KeyValuePair("tag2", 2), + ]; + var expectedTags = tags.ToImmutableArray().ToImmutableDictionary( + kvp => kvp.Key, + kvp => kvp.Value?.ToString() ?? string.Empty + ); + + // Act + var sut = _fixture.GetSut(); + sut.RecordMeasurement(instrument, measurement, tags, null); + + // Assert + _fixture.MockAggregator.Received().Distribution( + instrument.Name, + measurement, + MeasurementUnit.Custom(instrument.Unit!), + Arg.Is>(arg => + expectedTags.All(tag => arg.ContainsKey(tag.Key) && arg[tag.Key] == tag.Value) + ) + ); + } + + [Fact] + public void RecordMeasurement_ObservableGaugeInstrument_CallsGauge() + { + // Arrange + var testMeter = GetMeter(); + var instrument = testMeter.CreateObservableGauge(UniqueName(), () => new [] { new Measurement(2) }, "unit"); + const int measurement = 2; + ReadOnlySpan> tags = [ + new KeyValuePair("tag1", "value1"), + new KeyValuePair("tag2", 2), + ]; + var expectedTags = tags.ToImmutableArray().ToImmutableDictionary( + kvp => kvp.Key, + kvp => kvp.Value?.ToString() ?? string.Empty + ); + + // Act + var sut = _fixture.GetSut(); + sut.RecordMeasurement(instrument, measurement, tags, null); + + // Assert + _fixture.MockAggregator.Received().Gauge( + instrument.Name, + measurement, + MeasurementUnit.Custom(instrument.Unit!), + Arg.Is>(arg => + expectedTags.All(tag => arg.ContainsKey(tag.Key) && arg[tag.Key] == tag.Value) + ) + ); + } + + [Fact] + public void SystemDiagnosticsMetricsListener_Counter_AggregatesCorrectly() + { + // Arrange + var testMeter = GetMeter(); + var instrument = testMeter.CreateCounter(UniqueName(), "unit"); + _fixture.MetricsOptions.CaptureSystemDiagnosticsInstruments.Add(instrument.Name); + var total = 0d; + _fixture.MockAggregator.Increment( + instrument.Name, + Arg.Do(x => total += x), + Arg.Any(), + Arg.Any>()); + + // Act + var sut = _fixture.GetSut(); + instrument.Add(2); + instrument.Add(3); + + // Assert + _fixture.MockAggregator.Received(2).Increment( + instrument.Name, + Arg.Any(), + Arg.Any(), + Arg.Any>() + ); + total.Should().Be(5); + } + + [Fact] + public void SystemDiagnosticsMetricsListener_ObservableCounter_AggregatesCorrectly() + { + // Arrange + var testMeter = GetMeter(); + List> observedValues = [ new Measurement(2), new Measurement(3) ]; + var instrument = testMeter.CreateObservableCounter(UniqueName(), + () => observedValues); + _fixture.MetricsOptions.CaptureSystemDiagnosticsInstruments.Add(instrument.Name); + var total = 0d; + _fixture.MockAggregator.Increment( + instrument.Name, + Arg.Do(x => total += x), + Arg.Any(), + Arg.Any>()); + + // Act + var sut = _fixture.GetSut(); + sut._sentryListener.RecordObservableInstruments(); + + // Assert + _fixture.MockAggregator.Received(2).Increment( + instrument.Name, + Arg.Any(), + Arg.Any(), + Arg.Any>() + ); + total.Should().Be(5); + } + + [Fact] + public void SystemDiagnosticsMetricsListener_ObservableUpDownCounter_AggregatesCorrectly() + { + // Arrange + var testMeter = GetMeter(); + List> observedValues = [ new Measurement(12), new Measurement(-5) ]; + var instrument = testMeter.CreateObservableUpDownCounter(UniqueName(), + () => observedValues); + _fixture.MetricsOptions.CaptureSystemDiagnosticsInstruments.Add(instrument.Name); + var total = 0d; + _fixture.MockAggregator.Increment( + instrument.Name, + Arg.Do(x => total += x), + Arg.Any(), + Arg.Any>()); + + // Act + var sut = _fixture.GetSut(); + sut._sentryListener.RecordObservableInstruments(); + + // Assert + _fixture.MockAggregator.Received(2).Increment( + instrument.Name, + Arg.Any(), + Arg.Any(), + Arg.Any>() + ); + total.Should().Be(7); + } + + [Fact] + public void SystemDiagnosticsMetricsListener_OnlyListensToMatchingInstruments() + { + // Arrange + var testMeter = GetMeter(); + var match = testMeter.CreateCounter(UniqueName()); + var noMatch = testMeter.CreateCounter(UniqueName()); + _fixture.MetricsOptions.CaptureSystemDiagnosticsInstruments.Add(match.Name); + var total = 0d; + _fixture.MockAggregator.Increment( + Arg.Any(), + Arg.Do(x => total += x), + Arg.Any(), + Arg.Any>()); + + // Act + var sut = _fixture.GetSut(); + match.Add(5); + noMatch.Add(3); + + // Assert + _fixture.MockAggregator.Received(1).Increment( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>() + ); + total.Should().Be(5); + } + + [Fact] + public void SystemDiagnosticsMetricsListener_OnlyListensToMatchingMeters() + { + // Arrange + var matchingMeter = GetMeter(); + var matching1 = matchingMeter.CreateCounter(UniqueName()); + var matching2 = matchingMeter.CreateCounter(UniqueName()); + var nonMatchingMeter = GetMeter(); + var nonMatching1 = nonMatchingMeter.CreateCounter(UniqueName()); + _fixture.MetricsOptions.CaptureSystemDiagnosticsMeters.Add(matchingMeter.Name); + var total = 0d; + _fixture.MockAggregator.Increment( + Arg.Any(), + Arg.Do(x => total += x), + Arg.Any(), + Arg.Any>()); + + // Act + var sut = _fixture.GetSut(); + matching1.Add(3); + matching2.Add(5); + nonMatching1.Add(7); + + // Assert + _fixture.MockAggregator.Received(2).Increment( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any>() + ); + total.Should().Be(8); + } +} +#endif diff --git a/test/Sentry.Tests/SentryOptionsExtensionsTests.cs b/test/Sentry.Tests/SentryOptionsExtensionsTests.cs index 42b1427c5f..77253bf568 100644 --- a/test/Sentry.Tests/SentryOptionsExtensionsTests.cs +++ b/test/Sentry.Tests/SentryOptionsExtensionsTests.cs @@ -46,6 +46,16 @@ public void DisableTaskUnobservedTaskExceptionCapture_UnobservedTaskExceptionInt p => p is UnobservedTaskExceptionIntegration); } +#if NET8_0_OR_GREATER + [Fact] + public void DisableSystemDiagnosticsMetricsIntegration_RemovesSystemDiagnosticsMetricsIntegration() + { + Sut.DisableSystemDiagnosticsMetricsIntegration(); + Assert.DoesNotContain(Sut.Integrations, + p => p.GetType() == typeof(SystemDiagnosticsMetricsIntegration)); + } +#endif + [Fact] public void AddIntegration_StoredInOptions() { diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet.verified.txt index dfbc55fc32..b0305d9cb4 100644 --- a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet.verified.txt +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet.verified.txt @@ -34,5 +34,15 @@ Args: [ SentryDiagnosticListenerIntegration ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + SystemDiagnosticsMetricsIntegration + ] + }, + { + Level: info, + Message: System.Diagnostics.Metrics Integration is disabled because no listeners configured. } ] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet6_0.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet6_0.DotNet.verified.txt new file mode 100644 index 0000000000..dfbc55fc32 --- /dev/null +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet6_0.DotNet.verified.txt @@ -0,0 +1,38 @@ +[ + { + Message: Initializing Hub for Dsn: '{0}'., + Args: [ + https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AutoSessionTrackingIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainUnhandledExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainProcessExitIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + UnobservedTaskExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + SentryDiagnosticListenerIntegration + ] + } +] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet6_0.OSX.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet6_0.OSX.DotNet.verified.txt new file mode 100644 index 0000000000..dfbc55fc32 --- /dev/null +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet6_0.OSX.DotNet.verified.txt @@ -0,0 +1,38 @@ +[ + { + Message: Initializing Hub for Dsn: '{0}'., + Args: [ + https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AutoSessionTrackingIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainUnhandledExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainProcessExitIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + UnobservedTaskExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + SentryDiagnosticListenerIntegration + ] + } +] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet6_0.Windows.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet6_0.Windows.DotNet.verified.txt new file mode 100644 index 0000000000..10eaeaf749 --- /dev/null +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet6_0.Windows.DotNet.verified.txt @@ -0,0 +1,44 @@ +[ + { + Message: Initializing Hub for Dsn: '{0}'., + Args: [ + https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AutoSessionTrackingIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainUnhandledExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainProcessExitIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + UnobservedTaskExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + SentryDiagnosticListenerIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + WinUIUnhandledExceptionIntegration + ] + } +] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet7_0.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet7_0.DotNet.verified.txt new file mode 100644 index 0000000000..dfbc55fc32 --- /dev/null +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet7_0.DotNet.verified.txt @@ -0,0 +1,38 @@ +[ + { + Message: Initializing Hub for Dsn: '{0}'., + Args: [ + https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AutoSessionTrackingIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainUnhandledExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainProcessExitIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + UnobservedTaskExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + SentryDiagnosticListenerIntegration + ] + } +] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet7_0.OSX.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet7_0.OSX.DotNet.verified.txt new file mode 100644 index 0000000000..dfbc55fc32 --- /dev/null +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet7_0.OSX.DotNet.verified.txt @@ -0,0 +1,38 @@ +[ + { + Message: Initializing Hub for Dsn: '{0}'., + Args: [ + https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AutoSessionTrackingIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainUnhandledExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainProcessExitIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + UnobservedTaskExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + SentryDiagnosticListenerIntegration + ] + } +] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet7_0.Windows.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet7_0.Windows.DotNet.verified.txt new file mode 100644 index 0000000000..10eaeaf749 --- /dev/null +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet7_0.Windows.DotNet.verified.txt @@ -0,0 +1,44 @@ +[ + { + Message: Initializing Hub for Dsn: '{0}'., + Args: [ + https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AutoSessionTrackingIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainUnhandledExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainProcessExitIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + UnobservedTaskExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + SentryDiagnosticListenerIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + WinUIUnhandledExceptionIntegration + ] + } +] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.DotNet.verified.txt new file mode 100644 index 0000000000..9e3f2681b5 --- /dev/null +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.DotNet.verified.txt @@ -0,0 +1,48 @@ +[ + { + Message: Initializing Hub for Dsn: '{0}'., + Args: [ + https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AutoSessionTrackingIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainUnhandledExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainProcessExitIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + UnobservedTaskExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + SentryDiagnosticListenerIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + SystemDiagnosticsMetricsIntegration + ] + }, + { + Level: info, + Message: System.Diagnostics.Metrics Integration is disabled because no listeners are configured. + } +] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.OSX.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.OSX.DotNet.verified.txt new file mode 100644 index 0000000000..9e3f2681b5 --- /dev/null +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.OSX.DotNet.verified.txt @@ -0,0 +1,48 @@ +[ + { + Message: Initializing Hub for Dsn: '{0}'., + Args: [ + https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AutoSessionTrackingIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainUnhandledExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainProcessExitIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + UnobservedTaskExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + SentryDiagnosticListenerIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + SystemDiagnosticsMetricsIntegration + ] + }, + { + Level: info, + Message: System.Diagnostics.Metrics Integration is disabled because no listeners are configured. + } +] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.Windows.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.Windows.DotNet.verified.txt new file mode 100644 index 0000000000..e14cfaf075 --- /dev/null +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.Windows.DotNet.verified.txt @@ -0,0 +1,54 @@ +[ + { + Message: Initializing Hub for Dsn: '{0}'., + Args: [ + https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AutoSessionTrackingIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainUnhandledExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainProcessExitIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + UnobservedTaskExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + SentryDiagnosticListenerIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + WinUIUnhandledExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + SystemDiagnosticsMetricsIntegration + ] + }, + { + Level: info, + Message: System.Diagnostics.Metrics Integration is disabled because no listeners are configured. + } +] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.Net4_8.Mono.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.Net4_8.Mono.verified.txt new file mode 100644 index 0000000000..be7336d126 --- /dev/null +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.Net4_8.Mono.verified.txt @@ -0,0 +1,38 @@ +[ + { + Message: Initializing Hub for Dsn: '{0}'., + Args: [ + https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AutoSessionTrackingIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainUnhandledExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainProcessExitIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + UnobservedTaskExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + NetFxInstallationsIntegration + ] + } +] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.Net4_8.Net.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.Net4_8.Net.verified.txt new file mode 100644 index 0000000000..be7336d126 --- /dev/null +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.Net4_8.Net.verified.txt @@ -0,0 +1,38 @@ +[ + { + Message: Initializing Hub for Dsn: '{0}'., + Args: [ + https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AutoSessionTrackingIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainUnhandledExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainProcessExitIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + UnobservedTaskExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + NetFxInstallationsIntegration + ] + } +] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.Net4_8.OSX.Mono.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.Net4_8.OSX.Mono.verified.txt new file mode 100644 index 0000000000..be7336d126 --- /dev/null +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.Net4_8.OSX.Mono.verified.txt @@ -0,0 +1,38 @@ +[ + { + Message: Initializing Hub for Dsn: '{0}'., + Args: [ + https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AutoSessionTrackingIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainUnhandledExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainProcessExitIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + UnobservedTaskExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + NetFxInstallationsIntegration + ] + } +] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.Net4_8.Windows.Net.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.Net4_8.Windows.Net.verified.txt new file mode 100644 index 0000000000..be7336d126 --- /dev/null +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.Net4_8.Windows.Net.verified.txt @@ -0,0 +1,38 @@ +[ + { + Message: Initializing Hub for Dsn: '{0}'., + Args: [ + https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647 + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AutoSessionTrackingIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainUnhandledExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + AppDomainProcessExitIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + UnobservedTaskExceptionIntegration + ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + NetFxInstallationsIntegration + ] + } +] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.verify.cs b/test/Sentry.Tests/SentryOptionsTests.verify.cs index 3cfec63708..46f0bed53c 100644 --- a/test/Sentry.Tests/SentryOptionsTests.verify.cs +++ b/test/Sentry.Tests/SentryOptionsTests.verify.cs @@ -18,9 +18,12 @@ public Task Integrations_default_ones_are_properly_registered() }; Hub _ = new(options, Substitute.For()); - return Verify(logger.Entries) - .UniqueForOSPlatform() + var settingsTask = Verify(logger.Entries) + .UniqueForTargetFrameworkAndVersion() .UniqueForRuntime() .AutoVerify(includeBuildServer: false); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + settingsTask = settingsTask.UniqueForOSPlatform(); + return settingsTask; } } diff --git a/test/Sentry.Tests/SubstringOrRegexPatternTests.cs b/test/Sentry.Tests/SubstringOrRegexPatternTests.cs index f8cd8d1cee..6921e21e70 100644 --- a/test/Sentry.Tests/SubstringOrRegexPatternTests.cs +++ b/test/Sentry.Tests/SubstringOrRegexPatternTests.cs @@ -91,4 +91,20 @@ public void Regex_Pattern_Doesnt_Match_WhenCaseSensitive() var isMatch = target.IsMatch("aBcDeFgHi"); Assert.False(isMatch); } + + [Fact] + public void SubstringOrRegexPattern_ImplicitlyConvertsFromString() + { + SubstringOrRegexPattern target = "^abc.*ghi$"; + var isMatch = target.IsMatch("abcdefghi"); + Assert.True(isMatch); + } + + [Fact] + public void SubstringOrRegexPattern_ImplicitlyConvertsFromRegex() + { + SubstringOrRegexPattern target = new Regex("^abc.*ghi$"); + var isMatch = target.IsMatch("abcdefghi"); + Assert.True(isMatch); + } }