Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added StartTimer extension method to IMetricAggregator #3075

Merged
merged 11 commits into from
Jan 29, 2024
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ If you have conflicts, you can opt-out by adding the following to your `csproj`:
</PropertyGroup>
```

### Features
jamescrosswell marked this conversation as resolved.
Show resolved Hide resolved

- Added StartTimer extension method to IMetricAggregator ([#3075](https://github.com/getsentry/sentry-dotnet/pull/3075))
jamescrosswell marked this conversation as resolved.
Show resolved Hide resolved

### Fixes

- Moved the binding to MAUI events for breadcrumb creation from `WillFinishLaunching` to `FinishedLaunching`. This delays the initial instantiation of `app`. ([#3057](https://github.com/getsentry/sentry-dotnet/pull/3057))
Expand Down
32 changes: 15 additions & 17 deletions samples/Sentry.Samples.Console.Metrics/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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);
Expand All @@ -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++)
{
Expand All @@ -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++)
{
Expand All @@ -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++)
{
Expand Down
16 changes: 16 additions & 0 deletions src/Sentry/DisabledMetricAggregator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,22 @@ public void Timing(string key, double value, MeasurementUnit.Duration unit = Mea
// No Op
}

private class NoOpDisposable : IDisposable
{
public void Dispose()
{
// No Op
}
}

public IDisposable StartTimer(string key, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second,
IDictionary<string, string>? tags = null,
int stackLevel = 1)
{
// No Op
return new NoOpDisposable();
jamescrosswell marked this conversation as resolved.
Show resolved Hide resolved
}

public Task FlushAsync(bool force = true, CancellationToken cancellationToken = default)
{
// No Op
Expand Down
16 changes: 16 additions & 0 deletions src/Sentry/Extensibility/DisabledHub.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Sentry.Protocol.Metrics;

namespace Sentry.Extensibility;

/// <summary>
Expand Down Expand Up @@ -162,6 +164,20 @@ public void CaptureTransaction(SentryTransaction transaction, Scope? scope, Hint
{
}

/// <summary>
/// No-Op.
/// </summary>
public void CaptureMetrics(IEnumerable<Metric> metrics)
{
}

/// <summary>
/// No-Op.
/// </summary>
public void CaptureCodeLocations(CodeLocations codeLocations)
{
}

/// <summary>
/// No-Op.
/// </summary>
Expand Down
17 changes: 17 additions & 0 deletions src/Sentry/Extensibility/HubAdapter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Sentry.Infrastructure;
using Sentry.Protocol.Metrics;

namespace Sentry.Extensibility;

Expand Down Expand Up @@ -254,6 +255,22 @@ public void CaptureTransaction(SentryTransaction transaction)
public void CaptureTransaction(SentryTransaction transaction, Scope? scope, Hint? hint)
=> SentrySdk.CaptureTransaction(transaction, scope, hint);

/// <summary>
/// Forwards the call to <see cref="SentrySdk"/>.
/// </summary>
[DebuggerStepThrough]
[EditorBrowsable(EditorBrowsableState.Never)]
public void CaptureMetrics(IEnumerable<Metric> metrics)
=> SentrySdk.CaptureMetrics(metrics);

/// <summary>
/// Forwards the call to <see cref="SentrySdk"/>.
/// </summary>
[DebuggerStepThrough]
[EditorBrowsable(EditorBrowsableState.Never)]
public void CaptureCodeLocations(CodeLocations codeLocations)
=> SentrySdk.CaptureCodeLocations(codeLocations);

/// <summary>
/// Forwards the call to <see cref="SentrySdk"/>.
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions src/Sentry/IHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ TransactionContext ContinueTrace(
/// </summary>
void EndSession(SessionEndStatus status = SessionEndStatus.Exited);

/// <summary>
/// <inheritdoc cref="IMetricAggregator"/>
/// </summary>
IMetricAggregator Metrics { get; }

jamescrosswell marked this conversation as resolved.
Show resolved Hide resolved
/// <summary>
/// Captures an event with a configurable scope.
/// </summary>
Expand Down
12 changes: 12 additions & 0 deletions src/Sentry/IMetricAggregator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,18 @@ void Timing(string key,
DateTimeOffset? timestamp = null,
int stackLevel = 1);

/// <summary>
/// Measures the time it takes to run a given code block and emits this as a metric.
/// </summary>
/// <example>
/// using (SentrySdk.Metrics.StartTimer("my-operation"))
/// {
/// ...
/// }
/// </example>
IDisposable StartTimer(string key, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second,
IDictionary<string, string>? tags = null, int stackLevel = 1);

/// <summary>
/// Flushes any flushable metrics and/or code locations.
/// If <paramref name="force"/> is true then the cutoff is ignored and all metrics are flushed.
Expand Down
17 changes: 12 additions & 5 deletions src/Sentry/ISentryClient.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Sentry.Protocol.Metrics;

namespace Sentry;

/// <summary>
Expand Down Expand Up @@ -52,6 +54,16 @@ public interface ISentryClient
[EditorBrowsable(EditorBrowsableState.Never)]
void CaptureTransaction(SentryTransaction transaction, Scope? scope, Hint? hint);

/// <summary>
/// Captures one or more metrics to be sent to Sentry.
/// </summary>
void CaptureMetrics(IEnumerable<Metric> metrics);
jamescrosswell marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Captures one or more <see cref="CodeLocations"/> to be sent to Sentry.
/// </summary>
void CaptureCodeLocations(CodeLocations codeLocations);

/// <summary>
/// Captures a session update.
/// </summary>
Expand All @@ -68,9 +80,4 @@ public interface ISentryClient
/// <param name="timeout">The amount of time allowed for flushing.</param>
/// <returns>A task to await for the flush operation.</returns>
Task FlushAsync(TimeSpan timeout);

/// <summary>
/// <inheritdoc cref="IMetricAggregator"/>
/// </summary>
IMetricAggregator Metrics { get; }
}
58 changes: 56 additions & 2 deletions src/Sentry/Internal/Hub.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Sentry.Extensibility;
using Sentry.Infrastructure;
using Sentry.Protocol.Metrics;

namespace Sentry.Internal;

Expand Down Expand Up @@ -59,7 +60,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)
{
Expand Down Expand Up @@ -486,6 +494,52 @@ public void CaptureTransaction(SentryTransaction transaction, Scope? scope, Hint
}
}

public void CaptureMetrics(IEnumerable<Metric> metrics)
{
if (!IsEnabled)
{
return;
}

Metric[]? enumerable = null;
try
{
enumerable = metrics as Metric[] ?? metrics.ToArray();
_ownedClient.CaptureMetrics(enumerable);
}
catch (Exception e)
{
if (enumerable is null)
{
_options.LogError(e, "Failure to enumerate metrics for capture");
}
else
{
foreach (var metric in enumerable)
{
_options.LogError(e, "Failure to capture metric: {0}", metric.EventId);
jamescrosswell marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}

public void CaptureCodeLocations(CodeLocations codeLocations)
{
if (!IsEnabled)
{
return;
}

try
{
_ownedClient.CaptureCodeLocations(codeLocations);
}
catch (Exception e)
{
_options.LogError(e, "Failure to capture code locations: {0}", codeLocations.Timestamp);
jamescrosswell marked this conversation as resolved.
Show resolved Hide resolved
}
}

public void CaptureSession(SessionUpdate sessionUpdate)
{
if (!IsEnabled)
Expand Down Expand Up @@ -527,7 +581,7 @@ public void Dispose()

try
{
_ownedClient.Metrics.FlushAsync().ContinueWith(_ =>
Metrics.FlushAsync().ContinueWith(_ =>
_ownedClient.FlushAsync(_options.ShutdownTimeout).Wait()
).ConfigureAwait(false).GetAwaiter().GetResult();
}
Expand Down
23 changes: 12 additions & 11 deletions src/Sentry/MetricAggregator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ namespace Sentry;
internal class MetricAggregator : IMetricAggregator
{
private readonly SentryOptions _options;
private readonly Action<IEnumerable<Metric>> _captureMetrics;
private readonly Action<CodeLocations> _captureCodeLocations;
private readonly IHub _hub;
private readonly TimeSpan _flushInterval;

private readonly SemaphoreSlim _codeLocationLock = new(1,1);
Expand All @@ -36,20 +35,17 @@ private readonly Lazy<Dictionary<long, ConcurrentDictionary<string, Metric>>> _b
/// MetricAggregator constructor.
/// </summary>
/// <param name="options">The <see cref="SentryOptions"/></param>
/// <param name="captureMetrics">The callback to be called to transmit aggregated metrics</param>
/// <param name="captureCodeLocations">The callback to be called to transmit new code locations</param>
/// <param name="hub">The hub that should be used to create transactions and send data to Sentry</param>
/// <param name="shutdownSource">A <see cref="CancellationTokenSource"/></param>
/// <param name="disableLoopTask">
/// A boolean value indicating whether the Loop to flush metrics should run, for testing only.
/// </param>
/// <param name="flushInterval">An optional flushInterval, for testing only</param>
internal MetricAggregator(SentryOptions options, Action<IEnumerable<Metric>> captureMetrics,
Action<CodeLocations> captureCodeLocations, CancellationTokenSource? shutdownSource = null,
internal MetricAggregator(SentryOptions options, IHub hub, CancellationTokenSource? shutdownSource = null,
bool disableLoopTask = false, TimeSpan? flushInterval = null)
{
_options = options;
_captureMetrics = captureMetrics;
_captureCodeLocations = captureCodeLocations;
_hub = hub;
_shutdownSource = shutdownSource ?? new CancellationTokenSource();
_flushInterval = flushInterval ?? TimeSpan.FromSeconds(5);

Expand Down Expand Up @@ -161,6 +157,11 @@ public void Timing(string key,
DateTimeOffset? timestamp = null,
int stackLevel = 1) => Emit(MetricType.Distribution, key, value, unit, tags, timestamp, stackLevel + 1);

/// <inheritdoc cref="IMetricAggregator.StartTimer"/>
public IDisposable StartTimer(string key, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second,
IDictionary<string, string>? tags = null, int stackLevel = 1)
=> new Timing(_hub, key, unit, tags, stackLevel + 1);

private void Emit(
MetricType type,
string key,
Expand Down Expand Up @@ -231,7 +232,7 @@ private ConcurrentDictionary<string, Metric> GetOrAddTimeBucket(long bucketKey)
{
return existingBucket;
}

var timeBucket = new ConcurrentDictionary<string, Metric>();
Buckets[bucketKey] = timeBucket;
return timeBucket;
Expand Down Expand Up @@ -381,7 +382,7 @@ public async Task FlushAsync(bool force = true, CancellationToken cancellationTo
_bucketsLock.ExitWriteLock();
}

_captureMetrics(bucket.Values);
_hub.CaptureMetrics(bucket.Values);
_options.LogDebug("Metric flushed for bucket {0}", key);
}

Expand All @@ -391,7 +392,7 @@ public async Task FlushAsync(bool force = true, CancellationToken cancellationTo

_options.LogDebug("Flushing code locations: ", timestamp);
var codeLocations = new CodeLocations(timestamp, locations);
_captureCodeLocations(codeLocations);
_hub.CaptureCodeLocations(codeLocations);
_options.LogDebug("Code locations flushed: ", timestamp);
}

Expand Down
Loading