Skip to content

Commit

Permalink
Metrics Support - alpha (#2949)
Browse files Browse the repository at this point in the history
* Added basic metric types

* Moved Metric classes

* Added Increment aggregator

* Implemented Gauge metric

* Implemented Distribution and Set aggregations

* Added Metrics to ISentryClient API

* Update Hub.cs

* Verify tests

* Basic flush loop (no tests yet)

* Implemented statsd serialization

* Update CHANGELOG.md

* Split tests for Aggregagtor and BucketHelper

* Update MetricBucketHelperTests.cs

* Integrated review feedback

* Create MetricTests.verify.cs

* Updated verify tests

* Added Timing

* Added a (commented out) test to check if the aggregator is threadsafe

* Fixed concurrency issue (could still make this more performant)

* Reduced the scope of the lock for updating metrics

* Fixed unit tests

* Update MetricAggregator.cs

* Initial implementation of Code Locations

* Update Program.cs

* Updated solution filters

* Update CHANGELOG.md

* Changed Flush to FlushAsync

* Metrics now get flushed properly when disposing of the Hub

* Fixed serialization for code locations

* Update Timing.cs

* Clear stale seen periods at the end of each day

* Update CodeLocations.cs

* Fixed stacklevel when calling one of the two Timing constructors

* Removed IAsyncDisposable from MetricAggregator

* Cherry picked getsentry/Ben.Demystifier#4

* Update Ben.Demystifier

* Get line numbers with stack traces without enhanced stack traces

* Update Ben.Demystifier

* Reversed changes to AspNetCore.Basic sample (unrelated to this PR)

* Update Program.cs

* Improved the lock when incrementing/adding to existing metrics

* Tweaking docs

* Update CHANGELOG.md

Co-authored-by: Bruno Garcia <[email protected]>

* Integrating review feedback

* Source generated RegEx in metric helper

* Update Envelope.cs

* Review feedback

* Integrating review feedback

* More performant string delimited tags used in the bucket key

* Integrating review feedback

* Integrated review feedback

* Removed unused private field

---------

Co-authored-by: Bruno Garcia <[email protected]>
  • Loading branch information
jamescrosswell and bruno-garcia authored Jan 8, 2024
1 parent 8e298f8 commit 0fde2e3
Show file tree
Hide file tree
Showing 51 changed files with 1,982 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

### Features

- Experimental pre-release availability of Metrics. We're exploring the use of Metrics in Sentry. The API will very likely change and we don't yet have any documentation. ([#2949](https://github.com/getsentry/sentry-dotnet/pull/2949))
- MAUI Screenshot support. You can opt-in via `SentryMauiOptions.AttachScreenshots` ([#2965](https://github.com/getsentry/sentry-dotnet/pull/2965))
- Supports Android and iOS only. Windows is not supported.
- MAUI: App context has `in_foreground` indicating whether app was on the background or foreground. ([#2983](https://github.com/getsentry/sentry-dotnet/pull/2983))
Expand Down
1 change: 1 addition & 0 deletions Sentry-CI-Build-Linux.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"samples\\Sentry.Samples.Azure.Functions.Worker\\Sentry.Samples.Azure.Functions.Worker.csproj",
"samples\\Sentry.Samples.Console.Basic\\Sentry.Samples.Console.Basic.csproj",
"samples\\Sentry.Samples.Console.Customized\\Sentry.Samples.Console.Customized.csproj",
"samples\\Sentry.Samples.Console.Metrics\\Sentry.Samples.Console.Metrics.csproj",
"samples\\Sentry.Samples.Console.Native\\Sentry.Samples.Console.Native.csproj",
"samples\\Sentry.Samples.Console.Profiling\\Sentry.Samples.Console.Profiling.csproj",
"samples\\Sentry.Samples.EntityFramework\\Sentry.Samples.EntityFramework.csproj",
Expand Down
1 change: 1 addition & 0 deletions Sentry-CI-Build-Windows.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"samples\\Sentry.Samples.Azure.Functions.Worker\\Sentry.Samples.Azure.Functions.Worker.csproj",
"samples\\Sentry.Samples.Console.Basic\\Sentry.Samples.Console.Basic.csproj",
"samples\\Sentry.Samples.Console.Customized\\Sentry.Samples.Console.Customized.csproj",
"samples\\Sentry.Samples.Console.Metrics\\Sentry.Samples.Console.Metrics.csproj",
"samples\\Sentry.Samples.Console.Native\\Sentry.Samples.Console.Native.csproj",
"samples\\Sentry.Samples.Console.Profiling\\Sentry.Samples.Console.Profiling.csproj",
"samples\\Sentry.Samples.EntityFramework\\Sentry.Samples.EntityFramework.csproj",
Expand Down
1 change: 1 addition & 0 deletions Sentry-CI-Build-macOS.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"samples\\Sentry.Samples.Azure.Functions.Worker\\Sentry.Samples.Azure.Functions.Worker.csproj",
"samples\\Sentry.Samples.Console.Basic\\Sentry.Samples.Console.Basic.csproj",
"samples\\Sentry.Samples.Console.Customized\\Sentry.Samples.Console.Customized.csproj",
"samples\\Sentry.Samples.Console.Metrics\\Sentry.Samples.Console.Metrics.csproj",
"samples\\Sentry.Samples.Console.Native\\Sentry.Samples.Console.Native.csproj",
"samples\\Sentry.Samples.Console.Profiling\\Sentry.Samples.Console.Profiling.csproj",
"samples\\Sentry.Samples.EntityFramework\\Sentry.Samples.EntityFramework.csproj",
Expand Down
7 changes: 7 additions & 0 deletions Sentry.NoMobile.sln
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FastSerialization", "module
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Samples.Console.Native", "samples\Sentry.Samples.Console.Native\Sentry.Samples.Console.Native.csproj", "{FC8AEABA-1A40-4891-9EBA-4B6A1F7244B2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Samples.Console.Metrics", "samples\Sentry.Samples.Console.Metrics\Sentry.Samples.Console.Metrics.csproj", "{BD2D08FC-8675-4157-A73C-D75F6A3856D3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -465,6 +467,10 @@ Global
{FC8AEABA-1A40-4891-9EBA-4B6A1F7244B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FC8AEABA-1A40-4891-9EBA-4B6A1F7244B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FC8AEABA-1A40-4891-9EBA-4B6A1F7244B2}.Release|Any CPU.Build.0 = Release|Any CPU
{BD2D08FC-8675-4157-A73C-D75F6A3856D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BD2D08FC-8675-4157-A73C-D75F6A3856D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BD2D08FC-8675-4157-A73C-D75F6A3856D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BD2D08FC-8675-4157-A73C-D75F6A3856D3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -545,5 +551,6 @@ Global
{67269916-C417-4CEE-BD7D-CA66C3830AEE} = {A3CCA27E-4DF8-479D-833C-CAA0950715AA}
{8032310D-3C06-442C-A318-F365BCC4C804} = {A3CCA27E-4DF8-479D-833C-CAA0950715AA}
{FC8AEABA-1A40-4891-9EBA-4B6A1F7244B2} = {21B42F60-5802-404E-90F0-AEBCC56760C0}
{BD2D08FC-8675-4157-A73C-D75F6A3856D3} = {21B42F60-5802-404E-90F0-AEBCC56760C0}
EndGlobalSection
EndGlobal
7 changes: 7 additions & 0 deletions Sentry.sln
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FastSerialization", "module
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Samples.Console.Native", "samples\Sentry.Samples.Console.Native\Sentry.Samples.Console.Native.csproj", "{FC8AEABA-1A40-4891-9EBA-4B6A1F7244B2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sentry.Samples.Console.Metrics", "samples\Sentry.Samples.Console.Metrics\Sentry.Samples.Console.Metrics.csproj", "{BD2D08FC-8675-4157-A73C-D75F6A3856D3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -465,6 +467,10 @@ Global
{FC8AEABA-1A40-4891-9EBA-4B6A1F7244B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FC8AEABA-1A40-4891-9EBA-4B6A1F7244B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FC8AEABA-1A40-4891-9EBA-4B6A1F7244B2}.Release|Any CPU.Build.0 = Release|Any CPU
{BD2D08FC-8675-4157-A73C-D75F6A3856D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BD2D08FC-8675-4157-A73C-D75F6A3856D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BD2D08FC-8675-4157-A73C-D75F6A3856D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BD2D08FC-8675-4157-A73C-D75F6A3856D3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -545,5 +551,6 @@ Global
{67269916-C417-4CEE-BD7D-CA66C3830AEE} = {A3CCA27E-4DF8-479D-833C-CAA0950715AA}
{8032310D-3C06-442C-A318-F365BCC4C804} = {A3CCA27E-4DF8-479D-833C-CAA0950715AA}
{FC8AEABA-1A40-4891-9EBA-4B6A1F7244B2} = {21B42F60-5802-404E-90F0-AEBCC56760C0}
{BD2D08FC-8675-4157-A73C-D75F6A3856D3} = {21B42F60-5802-404E-90F0-AEBCC56760C0}
EndGlobalSection
EndGlobal
1 change: 1 addition & 0 deletions SentryNoMobile.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"samples\\Sentry.Samples.Azure.Functions.Worker\\Sentry.Samples.Azure.Functions.Worker.csproj",
"samples\\Sentry.Samples.Console.Basic\\Sentry.Samples.Console.Basic.csproj",
"samples\\Sentry.Samples.Console.Customized\\Sentry.Samples.Console.Customized.csproj",
"samples\\Sentry.Samples.Console.Metrics\\Sentry.Samples.Console.Metrics.csproj",
"samples\\Sentry.Samples.Console.Native\\Sentry.Samples.Console.Native.csproj",
"samples\\Sentry.Samples.Console.Profiling\\Sentry.Samples.Console.Profiling.csproj",
"samples\\Sentry.Samples.EntityFramework\\Sentry.Samples.EntityFramework.csproj",
Expand Down
2 changes: 1 addition & 1 deletion modules/Ben.Demystifier
107 changes: 107 additions & 0 deletions samples/Sentry.Samples.Console.Metrics/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System.Numerics;

namespace Sentry.Samples.Console.Metrics;

internal static class Program
{
private static readonly Random Roll = new();

private static void Main()
{
// Enable the SDK
using (SentrySdk.Init(options =>
{
options.Dsn =
// NOTE: ADD YOUR OWN DSN BELOW so you can see the events in your own Sentry account
"https://[email protected]/5428537";

options.Debug = true;
options.StackTraceMode = StackTraceMode.Enhanced;
// 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
};
}))
{
System.Console.WriteLine("Measure, Yeah, Measure!");
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;
}


// Optional: Delay to prevent tight looping
var sleepTime = Roll.Next(1, 10);
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;
}
}
System.Console.WriteLine("Measure up");
}
}

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
// 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))
{
for (var i = 0; i < attempts; i++)
{
var guess = Roll.Next(1, 100);
// This demonstrates the use of a set metric.
SentrySdk.Metrics.Gauge("guesses", guess);

// And this is a counter
SentrySdk.Metrics.Increment(solution.Contains(guess) ? "correct_answers" : "incorrect_answers");
}
}
}

private static void CreateRevenueGauge(int sampleCount)
{
using (new Timing(nameof(CreateRevenueGauge), MeasurementUnit.Duration.Millisecond))
{
for (var i = 0; i < sampleCount; i++)
{
var movement = Roll.NextDouble() * 30 - Roll.NextDouble() * 10;
// This demonstrates measuring something in your app using a gauge... we're also using a custom
// measurement unit here (which is optional - by default the unit will be "None")
SentrySdk.Metrics.Gauge("revenue", movement, MeasurementUnit.Custom("$"));
}
}
}

private static void MeasureShrimp(int sampleCount)
{
using (new Timing(nameof(MeasureShrimp), MeasurementUnit.Duration.Millisecond))
{
for (var i = 0; i < sampleCount; i++)
{
var sizeOfShrimp = 15 + Roll.NextDouble() * 30;
// This is an example of emitting a distribution metric
SentrySdk.Metrics.Distribution("shrimp.size", sizeOfShrimp, MeasurementUnit.Custom("cm"));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Sentry\Sentry.csproj" />
</ItemGroup>

</Project>
50 changes: 50 additions & 0 deletions src/Sentry/DisabledMetricAggregator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
namespace Sentry;

internal class DisabledMetricAggregator : IMetricAggregator
{
public void Increment(string key, double value = 1.0, MeasurementUnit? unit = null,
IDictionary<string, string>? tags = null,
DateTimeOffset? timestamp = null, int stackLevel = 1)
{
// No Op
}

public void Gauge(string key, double value = 1.0, MeasurementUnit? unit = null,
IDictionary<string, string>? tags = null,
DateTimeOffset? timestamp = null, int stackLevel = 1)
{
// No Op
}

public void Distribution(string key, double value = 1.0, MeasurementUnit? unit = null,
IDictionary<string, string>? tags = null,
DateTimeOffset? timestamp = null, int stackLevel = 1)
{
// No Op
}

public void Set(string key, int value, MeasurementUnit? unit = null,
IDictionary<string, string>? tags = null,
DateTimeOffset? timestamp = null, int stackLevel = 1)
{
// No Op
}

public void Timing(string key, double value, MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second,
IDictionary<string, string>? tags = null,
DateTimeOffset? timestamp = null, int stackLevel = 1)
{
// No Op
}

public Task FlushAsync(bool force = true, CancellationToken cancellationToken = default)
{
// No Op
return Task.CompletedTask;
}

public void Dispose()
{
// No Op
}
}
5 changes: 5 additions & 0 deletions src/Sentry/Extensibility/DisabledHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ public void CaptureSession(SessionUpdate sessionUpdate)
/// </summary>
public Task FlushAsync(TimeSpan timeout) => Task.CompletedTask;

/// <summary>
/// Disabled Metrics Aggregator (all methods are no-op).
/// </summary>
public IMetricAggregator Metrics { get; } = new DisabledMetricAggregator();

/// <summary>
/// No-Op.
/// </summary>
Expand Down
4 changes: 4 additions & 0 deletions src/Sentry/Extensibility/HubAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,10 @@ public void CaptureSession(SessionUpdate sessionUpdate)
public Task FlushAsync(TimeSpan timeout)
=> SentrySdk.FlushAsync(timeout);

/// <inheritdoc cref="IMetricAggregator"/>
public IMetricAggregator Metrics
=> SentrySdk.Metrics;

/// <summary>
/// Forwards the call to <see cref="SentrySdk"/>
/// </summary>
Expand Down
107 changes: 107 additions & 0 deletions src/Sentry/IMetricAggregator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
namespace Sentry;

/// <summary>
/// Exposes EXPERIMENTAL capability to emit metrics. This API is subject to change without major version bumps so use
/// with caution. We advise disabling in production at the moment.
/// </summary>
public interface IMetricAggregator: IDisposable
{
/// <summary>
/// Emits a Counter metric
/// </summary>
/// <param name="key">A unique key identifying the metric</param>
/// <param name="value">The value to be added</param>
/// <param name="unit">An optional <see cref="MeasurementUnit"/></param>
/// <param name="tags">Optional Tags to associate with the metric</param>
/// <param name="timestamp">
/// The time when the metric was emitted. Defaults to the time at which the metric is emitted, if no value is provided.
/// </param>
/// <param name="stackLevel">Optional number of stacks levels to ignore when determining the code location</param>
void Increment(string key,
double value = 1.0,
MeasurementUnit? unit = null,
IDictionary<string, string>? tags = null,
DateTimeOffset? timestamp = null,
int stackLevel = 1);

/// <summary>
/// Emits a Gauge metric
/// </summary>
/// <param name="key">A unique key identifying the metric</param>
/// <param name="value">The value to be added</param>
/// <param name="unit">An optional <see cref="MeasurementUnit"/></param>
/// <param name="tags">Optional Tags to associate with the metric</param>
/// <param name="timestamp">
/// The time when the metric was emitted. Defaults to the time at which the metric is emitted, if no value is provided.
/// </param>
/// <param name="stackLevel">Optional number of stacks levels to ignore when determining the code location</param>
void Gauge(string key,
double value = 1.0,
MeasurementUnit? unit = null,
IDictionary<string, string>? tags = null,
DateTimeOffset? timestamp = null,
int stackLevel = 1);

/// <summary>
/// Emits a Distribution metric
/// </summary>
/// <param name="key">A unique key identifying the metric</param>
/// <param name="value">The value to be added</param>
/// <param name="unit">An optional <see cref="MeasurementUnit"/></param>
/// <param name="tags">Optional Tags to associate with the metric</param>
/// <param name="timestamp">
/// The time when the metric was emitted. Defaults to the time at which the metric is emitted, if no value is provided.
/// </param>
/// <param name="stackLevel">Optional number of stacks levels to ignore when determining the code location</param>
void Distribution(string key,
double value = 1.0,
MeasurementUnit? unit = null,
IDictionary<string, string>? tags = null,
DateTimeOffset? timestamp = null,
int stackLevel = 1);

/// <summary>
/// Emits a Set metric
/// </summary>
/// <param name="key">A unique key identifying the metric</param>
/// <param name="value">The value to be added</param>
/// <param name="unit">An optional <see cref="MeasurementUnit"/></param>
/// <param name="tags">Optional Tags to associate with the metric</param>
/// <param name="timestamp">
/// The time when the metric was emitted. Defaults to the time at which the metric is emitted, if no value is provided.
/// </param>
/// <param name="stackLevel">Optional number of stacks levels to ignore when determining the code location</param>
void Set(string key,
int value,
MeasurementUnit? unit = null,
IDictionary<string, string>? tags = null,
DateTimeOffset? timestamp = null,
int stackLevel = 1);

/// <summary>
/// Emits a distribution with the time it takes to run a given code block.
/// </summary>
/// <param name="key">A unique key identifying the metric</param>
/// <param name="value">The value to be added</param>
/// <param name="unit">
/// An optional <see cref="MeasurementUnit.Duration"/>. Defaults to <see cref="MeasurementUnit.Duration.Second"/>
/// </param>
/// <param name="tags">Optional Tags to associate with the metric</param>
/// <param name="timestamp">The time when the metric was emitted</param>
/// <param name="stackLevel">Optional number of stacks levels to ignore when determining the code location</param>
void Timing(string key,
double value,
MeasurementUnit.Duration unit = MeasurementUnit.Duration.Second,
IDictionary<string, string>? tags = null,
DateTimeOffset? timestamp = 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.
/// </summary>
/// <param name="force">Forces all buckets to be flushed, ignoring the cutoff</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/></param>
/// <returns>False if a shutdown is requested during flush, true otherwise</returns>
Task FlushAsync(bool force = true, CancellationToken cancellationToken = default);
}
Loading

0 comments on commit 0fde2e3

Please sign in to comment.