Skip to content

Commit e0d87e4

Browse files
Improve code coverage for Metrics (#3094)
1 parent f47bac4 commit e0d87e4

File tree

6 files changed

+499
-51
lines changed

6 files changed

+499
-51
lines changed

src/Sentry/MetricAggregator.cs

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,15 @@ namespace Sentry;
88

99
internal class MetricAggregator : IMetricAggregator
1010
{
11+
internal const string DisposingMessage = "Disposing MetricAggregator.";
12+
internal const string AlreadyDisposedMessage = "Already disposed MetricAggregator.";
13+
internal const string CancelledMessage = "Stopping the Metric Aggregator due to a cancellation.";
14+
internal const string ShutdownScheduledMessage = "Shutdown scheduled. Stopping by: {0}.";
15+
internal const string ShutdownImmediatelyMessage = "Exiting immediately due to 0 shutdown timeout.";
16+
internal const string FlushShutdownMessage = "Shutdown token triggered. Exiting metric aggregator.";
17+
1118
private readonly SentryOptions _options;
1219
private readonly IMetricHub _metricHub;
13-
private readonly TimeSpan _flushInterval;
1420

1521
private readonly SemaphoreSlim _codeLocationLock = new(1,1);
1622
private readonly ReaderWriterLockSlim _bucketsLock = new ReaderWriterLockSlim();
@@ -26,20 +32,18 @@ internal class MetricAggregator : IMetricAggregator
2632
private readonly Lazy<Dictionary<long, ConcurrentDictionary<string, Metric>>> _buckets
2733
= new(() => new Dictionary<long, ConcurrentDictionary<string, Metric>>());
2834

29-
private long _lastClearedStaleLocations = DateTimeOffset.UtcNow.GetDayBucketKey();
30-
private readonly ConcurrentDictionary<long, HashSet<MetricResourceIdentifier>> _seenLocations = new();
31-
private Dictionary<long, Dictionary<MetricResourceIdentifier, SentryStackFrame>> _pendingLocations = new();
35+
internal long _lastClearedStaleLocations = DateTimeOffset.UtcNow.GetDayBucketKey();
36+
internal readonly ConcurrentDictionary<long, HashSet<MetricResourceIdentifier>> _seenLocations = new();
37+
internal Dictionary<long, Dictionary<MetricResourceIdentifier, SentryStackFrame>> _pendingLocations = new();
3238

33-
private readonly Task _loopTask;
39+
internal readonly Task _loopTask;
3440

3541
internal MetricAggregator(SentryOptions options, IMetricHub metricHub,
36-
CancellationTokenSource? shutdownSource = null,
37-
bool disableLoopTask = false, TimeSpan? flushInterval = null)
42+
CancellationTokenSource? shutdownSource = null, bool disableLoopTask = false)
3843
{
3944
_options = options;
4045
_metricHub = metricHub;
4146
_shutdownSource = shutdownSource ?? new CancellationTokenSource();
42-
_flushInterval = flushInterval ?? TimeSpan.FromSeconds(5);
4347

4448
if (disableLoopTask)
4549
{
@@ -157,7 +161,7 @@ public void Set(string key,
157161
}
158162

159163
/// <inheritdoc cref="IMetricAggregator.Timing"/>
160-
public void Timing(string key,
164+
public virtual void Timing(string key,
161165
double value,
162166
MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second,
163167
IDictionary<string, string>? tags = null,
@@ -321,12 +325,12 @@ private async Task RunLoopAsync()
321325
// If the cancellation was signaled, run until the end of the queue or shutdownTimeout
322326
try
323327
{
324-
await Task.Delay(_flushInterval, _shutdownSource.Token).ConfigureAwait(false);
328+
await Task.Delay(_options.ShutdownTimeout, _shutdownSource.Token).ConfigureAwait(false);
325329
}
326330
// Cancellation requested and no timeout allowed, so exit even if there are more items
327331
catch (OperationCanceledException) when (_options.ShutdownTimeout == TimeSpan.Zero)
328332
{
329-
_options.LogDebug("Exiting immediately due to 0 shutdown timeout.");
333+
_options.LogDebug(ShutdownImmediatelyMessage);
330334

331335
await shutdownTimeout.CancelAsync().ConfigureAwait(false);
332336

@@ -335,9 +339,7 @@ private async Task RunLoopAsync()
335339
// Cancellation requested, scheduled shutdown
336340
catch (OperationCanceledException)
337341
{
338-
_options.LogDebug(
339-
"Shutdown scheduled. Stopping by: {0}.",
340-
_options.ShutdownTimeout);
342+
_options.LogDebug(ShutdownScheduledMessage, _options.ShutdownTimeout);
341343

342344
shutdownTimeout.CancelAfterSafe(_options.ShutdownTimeout);
343345

@@ -407,15 +409,20 @@ public async Task FlushAsync(bool force = true, CancellationToken cancellationTo
407409
}
408410
catch (OperationCanceledException)
409411
{
410-
_options.LogInfo("Shutdown token triggered. Exiting metric aggregator.");
412+
_options.LogInfo(FlushShutdownMessage);
411413
}
412414
catch (Exception exception)
413415
{
414416
_options.LogError(exception, "Error processing metrics.");
415417
}
416418
finally
417419
{
418-
_flushLock.Release();
420+
// If the shutdown token was cancelled before we start this method, we can get here
421+
// without the _flushLock.CurrentCount (i.e. available threads) having been decremented
422+
if (_flushLock.CurrentCount < 1)
423+
{
424+
_flushLock.Release();
425+
}
419426
}
420427
}
421428

@@ -483,9 +490,9 @@ private Dictionary<long, Dictionary<MetricResourceIdentifier, SentryStackFrame>>
483490
/// <summary>
484491
/// Clear out stale seen locations once a day
485492
/// </summary>
486-
private void ClearStaleLocations()
493+
internal void ClearStaleLocations(DateTimeOffset? testNow = null)
487494
{
488-
var now = DateTimeOffset.UtcNow;
495+
var now = testNow ?? DateTimeOffset.UtcNow;
489496
var today = now.GetDayBucketKey();
490497
if (_lastClearedStaleLocations == today)
491498
{
@@ -511,11 +518,11 @@ private void ClearStaleLocations()
511518
/// <inheritdoc cref="IAsyncDisposable.DisposeAsync"/>
512519
public async ValueTask DisposeAsync()
513520
{
514-
_options.LogDebug("Disposing MetricAggregator.");
521+
_options.LogDebug(DisposingMessage);
515522

516523
if (_disposed)
517524
{
518-
_options.LogDebug("Already disposed MetricAggregator.");
525+
_options.LogDebug(AlreadyDisposedMessage);
519526
return;
520527
}
521528

@@ -534,7 +541,7 @@ public async ValueTask DisposeAsync()
534541
}
535542
catch (OperationCanceledException)
536543
{
537-
_options.LogDebug("Stopping the Metric Aggregator due to a cancellation.");
544+
_options.LogDebug(CancelledMessage);
538545
}
539546
catch (Exception exception)
540547
{

src/Sentry/MetricHelper.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ namespace Sentry;
55
internal static partial class MetricHelper
66
{
77
private static readonly RandomValuesFactory Random = new SynchronizedRandomValuesFactory();
8-
98
private const int RollupInSeconds = 10;
9+
private const string InvalidKeyCharactersPattern = @"[^a-zA-Z0-9_/.-]+";
10+
private const string InvalidValueCharactersPattern = @"[^\w\d_:/@\.\{\}\[\]$-]+";
1011

1112
#if NET6_0_OR_GREATER
1213
private static readonly DateTimeOffset UnixEpoch = DateTimeOffset.UnixEpoch;
@@ -40,17 +41,17 @@ internal static DateTimeOffset GetCutoff() => DateTimeOffset.UtcNow
4041
.Subtract(TimeSpan.FromMilliseconds(FlushShift));
4142

4243
#if NET7_0_OR_GREATER
43-
[GeneratedRegex(@"[^a-zA-Z0-9_/.-]+", RegexOptions.Compiled)]
44+
[GeneratedRegex(InvalidKeyCharactersPattern, RegexOptions.Compiled)]
4445
private static partial Regex InvalidKeyCharacters();
4546
internal static string SanitizeKey(string input) => InvalidKeyCharacters().Replace(input, "_");
4647

47-
[GeneratedRegex(@"[^\w\d_:/@\.\{\}\[\]$-]+", RegexOptions.Compiled)]
48+
[GeneratedRegex(InvalidValueCharactersPattern, RegexOptions.Compiled)]
4849
private static partial Regex InvalidValueCharacters();
4950
internal static string SanitizeValue(string input) => InvalidValueCharacters().Replace(input, "_");
5051
#else
51-
private static readonly Regex InvalidKeyCharacters = new(@"[^a-zA-Z0-9_/.-]+", RegexOptions.Compiled);
52+
private static readonly Regex InvalidKeyCharacters = new(InvalidKeyCharactersPattern, RegexOptions.Compiled);
5253
internal static string SanitizeKey(string input) => InvalidKeyCharacters.Replace(input, "_");
53-
private static readonly Regex InvalidValueCharacters = new(@"[^\w\d_:/@\.\{\}\[\]$-]+", RegexOptions.Compiled);
54+
private static readonly Regex InvalidValueCharacters = new(InvalidValueCharactersPattern, RegexOptions.Compiled);
5455
internal static string SanitizeValue(string input) => InvalidValueCharacters.Replace(input, "_");
5556
#endif
5657
}

src/Sentry/Timing.cs

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,31 @@ namespace Sentry;
1414
/// </example>
1515
internal class Timing : IDisposable
1616
{
17-
private readonly IMetricHub _metricHub;
17+
internal const string OperationName = "metric.timing";
18+
1819
private readonly SentryOptions _options;
1920
private readonly MetricAggregator _metricAggregator;
2021
private readonly string _key;
2122
private readonly MeasurementUnit.Duration _unit;
2223
private readonly IDictionary<string, string>? _tags;
23-
private readonly Stopwatch _stopwatch = new();
24+
internal readonly Stopwatch _stopwatch = new();
2425
private readonly ISpan _span;
25-
private readonly DateTime _startTime = DateTime.UtcNow;
26+
internal readonly DateTime _startTime = DateTime.UtcNow;
2627

2728
/// <summary>
2829
/// Creates a new <see cref="Timing"/> instance.
2930
/// </summary>
3031
internal Timing(MetricAggregator metricAggregator, IMetricHub metricHub, SentryOptions options,
3132
string key, MeasurementUnit.Duration unit, IDictionary<string, string>? tags, int stackLevel)
3233
{
33-
_metricHub = metricHub;
3434
_options = options;
3535
_metricAggregator = metricAggregator;
3636
_key = key;
3737
_unit = unit;
3838
_tags = tags;
3939
_stopwatch.Start();
4040

41-
42-
_span = metricHub.StartSpan("metric.timing", key);
41+
_span = metricHub.StartSpan(OperationName, key);
4342
if (tags is not null)
4443
{
4544
_span.SetTags(tags);
@@ -53,19 +52,23 @@ internal Timing(MetricAggregator metricAggregator, IMetricHub metricHub, SentryO
5352
public void Dispose()
5453
{
5554
_stopwatch.Stop();
55+
DisposeInternal(_stopwatch.Elapsed);
56+
}
5657

58+
internal void DisposeInternal(TimeSpan elapsed)
59+
{
5760
try
5861
{
5962
var value = _unit switch
6063
{
61-
MeasurementUnit.Duration.Week => _stopwatch.Elapsed.TotalDays / 7,
62-
MeasurementUnit.Duration.Day => _stopwatch.Elapsed.TotalDays,
63-
MeasurementUnit.Duration.Hour => _stopwatch.Elapsed.TotalHours,
64-
MeasurementUnit.Duration.Minute => _stopwatch.Elapsed.TotalMinutes,
65-
MeasurementUnit.Duration.Second => _stopwatch.Elapsed.TotalSeconds,
66-
MeasurementUnit.Duration.Millisecond => _stopwatch.Elapsed.TotalMilliseconds,
67-
MeasurementUnit.Duration.Microsecond => _stopwatch.Elapsed.TotalMilliseconds * 1000,
68-
MeasurementUnit.Duration.Nanosecond => _stopwatch.Elapsed.TotalMilliseconds * 1000000,
64+
MeasurementUnit.Duration.Week => elapsed.TotalDays / 7,
65+
MeasurementUnit.Duration.Day => elapsed.TotalDays,
66+
MeasurementUnit.Duration.Hour => elapsed.TotalHours,
67+
MeasurementUnit.Duration.Minute => elapsed.TotalMinutes,
68+
MeasurementUnit.Duration.Second => elapsed.TotalSeconds,
69+
MeasurementUnit.Duration.Millisecond => elapsed.TotalMilliseconds,
70+
MeasurementUnit.Duration.Microsecond => elapsed.TotalMilliseconds * 1000,
71+
MeasurementUnit.Duration.Nanosecond => elapsed.TotalMilliseconds * 1000000,
6972
_ => throw new ArgumentOutOfRangeException(nameof(_unit), _unit, null)
7073
};
7174
_metricAggregator.Timing(_key, value, _unit, _tags, _startTime);

0 commit comments

Comments
 (0)