Skip to content

Commit 741e088

Browse files
Added StartTimer extension method to IMetricAggregator (#3075)
1 parent c7a8029 commit 741e088

24 files changed

+321
-168
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ If you have conflicts, you can opt-out by adding the following to your `csproj`:
1212
</PropertyGroup>
1313
```
1414

15+
### Features
16+
17+
- Timing metrics can now be captured with `SentrySdk.Metrics.StartTimer` ([#3075](https://github.com/getsentry/sentry-dotnet/pull/3075))
18+
1519
### Fixes
1620

1721
- Fixed an issue with tag values in metrics not being properly serialized ([#3065](https://github.com/getsentry/sentry-dotnet/pull/3065))

samples/Sentry.Samples.Console.Metrics/Program.cs

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ private static void Main()
1515

1616
options.Debug = true;
1717
options.StackTraceMode = StackTraceMode.Enhanced;
18+
options.SampleRate = 1.0f; // Not recommended in production - may adversely impact quota
19+
options.TracesSampleRate = 1.0f; // Not recommended in production - may adversely impact quota
1820
// Initialize some (non null) ExperimentalMetricsOptions to enable Sentry Metrics,
1921
options.ExperimentalMetrics = new ExperimentalMetricsOptions
2022
{
@@ -24,22 +26,17 @@ private static void Main()
2426
}))
2527
{
2628
System.Console.WriteLine("Measure, Yeah, Measure!");
29+
Action[] actions =
30+
[
31+
() => PlaySetBingo(10),
32+
() => CreateRevenueGauge(100),
33+
() => MeasureShrimp(30),
34+
];
2735
while (true)
2836
{
2937
// Perform your task here
30-
switch (Roll.Next(1,3))
31-
{
32-
case 1:
33-
PlaySetBingo(10);
34-
break;
35-
case 2:
36-
CreateRevenueGauge(100);
37-
break;
38-
case 3:
39-
MeasureShrimp(30);
40-
break;
41-
}
42-
38+
var actionIdx = Roll.Next(0, actions.Length);
39+
actions[actionIdx]();
4340

4441
// Optional: Delay to prevent tight looping
4542
var sleepTime = Roll.Next(1, 10);
@@ -60,9 +57,10 @@ private static void PlaySetBingo(int attempts)
6057
{
6158
var solution = new[] { 3, 5, 7, 11, 13, 17 };
6259

63-
// The Timing class creates a distribution that is designed to measure the amount of time it takes to run code
60+
// StartTimer creates a distribution that is designed to measure the amount of time it takes to run code
6461
// blocks. By default it will use a unit of Seconds - we're configuring it to use milliseconds here though.
65-
using (new Timing("bingo", MeasurementUnit.Duration.Millisecond))
62+
// The return value is an IDisposable and the timer will stop when the timer is disposed of.
63+
using (SentrySdk.Metrics.StartTimer("bingo", MeasurementUnit.Duration.Millisecond))
6664
{
6765
for (var i = 0; i < attempts; i++)
6866
{
@@ -78,7 +76,7 @@ private static void PlaySetBingo(int attempts)
7876

7977
private static void CreateRevenueGauge(int sampleCount)
8078
{
81-
using (new Timing(nameof(CreateRevenueGauge), MeasurementUnit.Duration.Millisecond))
79+
using (SentrySdk.Metrics.StartTimer(nameof(CreateRevenueGauge), MeasurementUnit.Duration.Millisecond))
8280
{
8381
for (var i = 0; i < sampleCount; i++)
8482
{
@@ -92,7 +90,7 @@ private static void CreateRevenueGauge(int sampleCount)
9290

9391
private static void MeasureShrimp(int sampleCount)
9492
{
95-
using (new Timing(nameof(MeasureShrimp), MeasurementUnit.Duration.Millisecond))
93+
using (SentrySdk.Metrics.StartTimer(nameof(MeasureShrimp), MeasurementUnit.Duration.Millisecond))
9694
{
9795
for (var i = 0; i < sampleCount; i++)
9896
{

src/Sentry/DisabledMetricAggregator.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ public void Timing(string key, double value, MeasurementUnit.Duration unit = Mea
3737
// No Op
3838
}
3939

40+
public IDisposable StartTimer(string key, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second,
41+
IDictionary<string, string>? tags = null,
42+
int stackLevel = 1)
43+
{
44+
// No Op
45+
return NoOpDisposable.Instance;
46+
}
47+
4048
public Task FlushAsync(bool force = true, CancellationToken cancellationToken = default)
4149
{
4250
// No Op
@@ -48,3 +56,14 @@ public void Dispose()
4856
// No Op
4957
}
5058
}
59+
60+
internal class NoOpDisposable : IDisposable
61+
{
62+
private static readonly Lazy<NoOpDisposable> LazyInstance = new();
63+
internal static NoOpDisposable Instance => LazyInstance.Value;
64+
65+
public void Dispose()
66+
{
67+
// No Op
68+
}
69+
}

src/Sentry/Extensibility/DisabledHub.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
using Sentry.Protocol.Envelopes;
2+
using Sentry.Protocol.Metrics;
3+
14
namespace Sentry.Extensibility;
25

36
/// <summary>
@@ -133,6 +136,14 @@ public void BindClient(ISentryClient client)
133136
{
134137
}
135138

139+
/// <summary>
140+
/// No-Op.
141+
/// </summary>
142+
public bool CaptureEnvelope(Envelope envelope)
143+
{
144+
return false;
145+
}
146+
136147
/// <summary>
137148
/// No-Op.
138149
/// </summary>

src/Sentry/Extensibility/HubAdapter.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using Sentry.Infrastructure;
2+
using Sentry.Protocol.Envelopes;
3+
using Sentry.Protocol.Metrics;
24

35
namespace Sentry.Extensibility;
46

@@ -209,6 +211,9 @@ public SentryId CaptureEvent(SentryEvent evt)
209211
public SentryId CaptureEvent(SentryEvent evt, Scope? scope)
210212
=> SentrySdk.CaptureEvent(evt, scope, null);
211213

214+
/// <inheritdoc cref="ISentryClient.CaptureEnvelope"/>
215+
public bool CaptureEnvelope(Envelope envelope) => SentrySdk.CurrentHub.CaptureEnvelope(envelope);
216+
212217
/// <summary>
213218
/// Forwards the call to <see cref="SentrySdk"/>.
214219
/// </summary>

src/Sentry/IHub.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using Sentry.Protocol.Metrics;
2+
13
namespace Sentry;
24

35
/// <summary>
@@ -19,6 +21,11 @@ public interface IHub :
1921
/// </summary>
2022
SentryId LastEventId { get; }
2123

24+
/// <summary>
25+
/// <inheritdoc cref="IMetricAggregator"/>
26+
/// </summary>
27+
IMetricAggregator Metrics { get; }
28+
2229
/// <summary>
2330
/// Starts a transaction.
2431
/// </summary>

src/Sentry/IMetricAggregator.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,18 @@ void Timing(string key,
9696
DateTimeOffset? timestamp = null,
9797
int stackLevel = 1);
9898

99+
/// <summary>
100+
/// Measures the time it takes to run a given code block and emits this as a metric.
101+
/// </summary>
102+
/// <example>
103+
/// using (SentrySdk.Metrics.StartTimer("my-operation"))
104+
/// {
105+
/// ...
106+
/// }
107+
/// </example>
108+
IDisposable StartTimer(string key, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second,
109+
IDictionary<string, string>? tags = null, int stackLevel = 1);
110+
99111
/// <summary>
100112
/// Flushes any flushable metrics and/or code locations.
101113
/// If <paramref name="force"/> is true then the cutoff is ignored and all metrics are flushed.

src/Sentry/IMetricHub.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Sentry.Protocol.Metrics;
2+
3+
namespace Sentry;
4+
5+
internal interface IMetricHub
6+
{
7+
/// <summary>
8+
/// Captures one or more metrics to be sent to Sentry.
9+
/// </summary>
10+
void CaptureMetrics(IEnumerable<Metric> metrics);
11+
12+
/// <summary>
13+
/// Captures one or more <see cref="CodeLocations"/> to be sent to Sentry.
14+
/// </summary>
15+
void CaptureCodeLocations(CodeLocations codeLocations);
16+
17+
/// <summary>
18+
/// Starts a child span for the current transaction or, if there is no active transaction, starts a new transaction.
19+
/// </summary>
20+
ISpan StartSpan(string operation, string description);
21+
}

src/Sentry/ISentryClient.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
using Sentry.Protocol.Envelopes;
2+
using Sentry.Protocol.Metrics;
3+
14
namespace Sentry;
25

36
/// <summary>
@@ -10,6 +13,13 @@ public interface ISentryClient
1013
/// </summary>
1114
bool IsEnabled { get; }
1215

16+
/// <summary>
17+
/// Capture an envelope and queue it.
18+
/// </summary>
19+
/// <param name="envelope">The envelope.</param>
20+
/// <returns>true if the enveloped was queued, false otherwise.</returns>
21+
bool CaptureEnvelope(Envelope envelope);
22+
1323
/// <summary>
1424
/// Capture the event
1525
/// </summary>
@@ -68,9 +78,4 @@ public interface ISentryClient
6878
/// <param name="timeout">The amount of time allowed for flushing.</param>
6979
/// <returns>A task to await for the flush operation.</returns>
7080
Task FlushAsync(TimeSpan timeout);
71-
72-
/// <summary>
73-
/// <inheritdoc cref="IMetricAggregator"/>
74-
/// </summary>
75-
IMetricAggregator Metrics { get; }
7681
}

src/Sentry/Internal/Hub.cs

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
using Sentry.Extensibility;
22
using Sentry.Infrastructure;
3+
using Sentry.Protocol.Envelopes;
4+
using Sentry.Protocol.Metrics;
35

46
namespace Sentry.Internal;
57

6-
internal class Hub : IHub, IDisposable
8+
internal class Hub : IHub, IMetricHub, IDisposable
79
{
810
private readonly object _sessionPauseLock = new();
911

@@ -59,7 +61,14 @@ internal Hub(
5961
PushScope();
6062
}
6163

62-
Metrics = _ownedClient.Metrics;
64+
if (options.ExperimentalMetrics is not null)
65+
{
66+
Metrics = new MetricAggregator(options, this);
67+
}
68+
else
69+
{
70+
Metrics = new DisabledMetricAggregator();
71+
}
6372

6473
foreach (var integration in options.Integrations)
6574
{
@@ -394,6 +403,8 @@ public SentryId CaptureEvent(SentryEvent evt, Hint? hint, Action<Scope> configur
394403
}
395404
}
396405

406+
public bool CaptureEnvelope(Envelope envelope) => _ownedClient.CaptureEnvelope(envelope);
407+
397408
public SentryId CaptureEvent(SentryEvent evt, Scope? scope = null, Hint? hint = null)
398409
{
399410
if (!IsEnabled)
@@ -486,6 +497,57 @@ public void CaptureTransaction(SentryTransaction transaction, Scope? scope, Hint
486497
}
487498
}
488499

500+
/// <inheritdoc cref="IMetricHub.CaptureMetrics"/>
501+
public void CaptureMetrics(IEnumerable<Metric> metrics)
502+
{
503+
if (!IsEnabled)
504+
{
505+
return;
506+
}
507+
508+
Metric[]? enumerable = null;
509+
try
510+
{
511+
enumerable = metrics as Metric[] ?? metrics.ToArray();
512+
_options.LogDebug("Capturing metrics.");
513+
_ownedClient.CaptureEnvelope(Envelope.FromMetrics(metrics));
514+
}
515+
catch (Exception e)
516+
{
517+
var metricEventIds = enumerable?.Select(m => m.EventId).ToArray() ?? [];
518+
_options.LogError(e, "Failure to capture metrics: {0}", string.Join(",", metricEventIds));
519+
}
520+
}
521+
522+
/// <inheritdoc cref="IMetricHub.CaptureCodeLocations"/>
523+
public void CaptureCodeLocations(CodeLocations codeLocations)
524+
{
525+
if (!IsEnabled)
526+
{
527+
return;
528+
}
529+
530+
try
531+
{
532+
_options.LogDebug("Capturing code locations for period: {0}", codeLocations.Timestamp);
533+
_ownedClient.CaptureEnvelope(Envelope.FromCodeLocations(codeLocations));
534+
}
535+
catch (Exception e)
536+
{
537+
_options.LogError(e, "Failure to capture code locations");
538+
}
539+
}
540+
541+
/// <inheritdoc cref="IMetricHub.StartSpan"/>
542+
public ISpan StartSpan(string operation, string description)
543+
{
544+
ITransactionTracer? currentTransaction = null;
545+
ConfigureScope(s => currentTransaction = s.Transaction);
546+
return currentTransaction is {} transaction
547+
? transaction.StartChild(operation, description)
548+
: this.StartTransaction(operation, description);
549+
}
550+
489551
public void CaptureSession(SessionUpdate sessionUpdate)
490552
{
491553
if (!IsEnabled)
@@ -527,7 +589,7 @@ public void Dispose()
527589

528590
try
529591
{
530-
_ownedClient.Metrics.FlushAsync().ContinueWith(_ =>
592+
Metrics.FlushAsync().ContinueWith(_ =>
531593
_ownedClient.FlushAsync(_options.ShutdownTimeout).Wait()
532594
).ConfigureAwait(false).GetAwaiter().GetResult();
533595
}

0 commit comments

Comments
 (0)