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

- Timing metrics can now be captured with `SentrySdk.Metrics.StartTimer` ([#3075](https://github.com/getsentry/sentry-dotnet/pull/3075))

### 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
19 changes: 19 additions & 0 deletions src/Sentry/DisabledMetricAggregator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>? tags = null,
int stackLevel = 1)
{
// No Op
return NoOpDisposable.Instance;
}

public Task FlushAsync(bool force = true, CancellationToken cancellationToken = default)
{
// No Op
Expand All @@ -48,3 +56,14 @@ public void Dispose()
// No Op
}
}

internal class NoOpDisposable : IDisposable
{
private static readonly Lazy<NoOpDisposable> LazyInstance = new();
internal static NoOpDisposable Instance => LazyInstance.Value;

public void Dispose()
{
// No Op
}
}
2 changes: 2 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
1 change: 1 addition & 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
7 changes: 7 additions & 0 deletions src/Sentry/IHub.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Sentry.Protocol.Metrics;

namespace Sentry;

/// <summary>
Expand All @@ -19,6 +21,11 @@ public interface IHub :
/// </summary>
SentryId LastEventId { get; }

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

/// <summary>
/// Starts a transaction.
/// </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
21 changes: 21 additions & 0 deletions src/Sentry/IMetricHub.cs
bitsandfoxes marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Sentry.Protocol.Metrics;

namespace Sentry;

internal interface IMetricHub
{
/// <summary>
/// Captures one or more metrics to be sent to Sentry.
/// </summary>
void CaptureMetrics(IEnumerable<Metric> metrics);

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

/// <summary>
/// Starts a child span for the current transaction or, if there is no active transaction, starts a new transaction.
/// </summary>
ISpan StartSpan(string operation, string description);
}
8 changes: 3 additions & 5 deletions src/Sentry/ISentryClient.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using Sentry.Protocol.Envelopes;
using Sentry.Protocol.Metrics;

namespace Sentry;

/// <summary>
Expand Down Expand Up @@ -68,9 +71,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; }
}
78 changes: 75 additions & 3 deletions src/Sentry/Internal/Hub.cs
Original file line number Diff line number Diff line change
@@ -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();

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

/// <inheritdoc cref="IMetricHub.CaptureMetrics"/>
public void CaptureMetrics(IEnumerable<Metric> metrics)
{
if (!IsEnabled)
{
return;
}

if (_ownedClient is not SentryClient sentryClient)
{
_options.LogDebug("Capturing envelopes not supported by this client.");
return;
}

Metric[]? enumerable = null;
try
{
enumerable = metrics as Metric[] ?? metrics.ToArray();
_options.LogDebug("Capturing metrics.");
sentryClient.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));
}
}

/// <inheritdoc cref="IMetricHub.CaptureCodeLocations"/>
public void CaptureCodeLocations(CodeLocations codeLocations)
{
if (!IsEnabled)
{
return;
}

if (_ownedClient is not SentryClient sentryClient)
{
_options.LogDebug("Capturing envelopes not supported by this client.");
return;
}

try
{
_options.LogDebug("Capturing code locations for period: {0}", codeLocations.Timestamp);
sentryClient.CaptureEnvelope(Envelope.FromCodeLocations(codeLocations));
}
catch (Exception e)
{
_options.LogError(e, "Failure to capture code locations");
}
}

/// <inheritdoc cref="IMetricHub.StartSpan"/>
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)
Expand Down Expand Up @@ -527,7 +599,7 @@ public void Dispose()

try
{
_ownedClient.Metrics.FlushAsync().ContinueWith(_ =>
Metrics.FlushAsync().ContinueWith(_ =>
_ownedClient.FlushAsync(_options.ShutdownTimeout).Wait()
).ConfigureAwait(false).GetAwaiter().GetResult();
}
Expand Down
34 changes: 13 additions & 21 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 IMetricHub _metricHub;
private readonly TimeSpan _flushInterval;

private readonly SemaphoreSlim _codeLocationLock = new(1,1);
Expand All @@ -32,24 +31,12 @@ private readonly Lazy<Dictionary<long, ConcurrentDictionary<string, Metric>>> _b

private readonly Task _loopTask;

/// <summary>
/// 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="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, 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);

Expand Down Expand Up @@ -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);

/// <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(this, _metricHub, _options, key, unit, tags, stackLevel + 1);

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

var timeBucket = new ConcurrentDictionary<string, Metric>();
Buckets[bucketKey] = timeBucket;
return timeBucket;
Expand All @@ -247,7 +239,7 @@ private ConcurrentDictionary<string, Metric> GetOrAddTimeBucket(long bucketKey)
}
}

internal void RecordCodeLocation(
internal virtual void RecordCodeLocation(
MetricType type,
string key,
MeasurementUnit unit,
Expand Down Expand Up @@ -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);
}

Expand All @@ -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);
}

Expand Down
Loading
Loading