Skip to content

Commit

Permalink
Merge branch 'main' into feat/metrics-set
Browse files Browse the repository at this point in the history
  • Loading branch information
bitsandfoxes committed Jan 30, 2024
2 parents 28eeba7 + 467676e commit b832505
Show file tree
Hide file tree
Showing 51 changed files with 1,902 additions and 200 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Features

- Added support for capturing built in metrics from the System.Diagnostics.Metrics API ([#3052](https://github.com/getsentry/sentry-dotnet/pull/3052))

### Significant change in behavior

- Added `Sentry` namespace to global usings when `ImplicitUsings` is enabled ([#3043](https://github.com/getsentry/sentry-dotnet/pull/3043))
Expand All @@ -15,6 +19,7 @@ If you have conflicts, you can opt-out by adding the following to your `csproj`:
### Features

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

### Fixes

Expand Down
98 changes: 66 additions & 32 deletions samples/Sentry.Samples.Console.Metrics/Program.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
namespace Sentry.Samples.Console.Metrics;
using System.Diagnostics.Metrics;
using System.Text.RegularExpressions;

namespace Sentry.Samples.Console.Metrics;

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

// Sentry also supports capturing System.Diagnostics.Metrics
private static readonly Meter HatsMeter = new("HatCo.HatStore", "1.0.0");
private static readonly Counter<int> HatsSold = HatsMeter.CreateCounter<int>(
name: "hats-sold",
unit: "Hats",
description: "The number of hats sold in our store");

private static void Main()
private static async Task Main()
{
// Enable the SDK
using (SentrySdk.Init(options =>
Expand All @@ -15,54 +25,55 @@ 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
{
EnableCodeLocations =
true // Set this to false if you don't want to track code locations for some reason
EnableCodeLocations = true, // Set this to false if you don't want to track code locations
CaptureSystemDiagnosticsInstruments = [
// Capture System.Diagnostics.Metrics matching the name "HatCo.HatStore", which is the name
// of the custom HatsMeter defined above
"hats-sold"
],
// Capture all built in metrics (this is the default - you can override this to capture some or
// none of these if you prefer)
CaptureSystemDiagnosticsMeters = BuiltInSystemDiagnosticsMeters.All
};
}))
{
System.Console.WriteLine("Measure, Yeah, Measure!");
while (true)

Action[] actions = [PlaySetBingo, CreateRevenueGauge, MeasureShrimp, SellHats];
do
{
// Perform your task here
switch (Roll.Next(1,3))
{
case 1:
PlaySetBingo(10);
break;
case 2:
CreateRevenueGauge(100);
break;
case 3:
MeasureShrimp(30);
break;
}
// Run a random action
var idx = Roll.Next(0, actions.Length);
actions[idx]();

// Make an API call
await CallSampleApiAsync();

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

private static void PlaySetBingo(int attempts)
private static void PlaySetBingo()
{
const int attempts = 10;
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 @@ -76,9 +87,10 @@ private static void PlaySetBingo(int attempts)
}
}

private static void CreateRevenueGauge(int sampleCount)
private static void CreateRevenueGauge()
{
using (new Timing(nameof(CreateRevenueGauge), MeasurementUnit.Duration.Millisecond))
const int sampleCount = 100;
using (SentrySdk.Metrics.StartTimer(nameof(CreateRevenueGauge), MeasurementUnit.Duration.Millisecond))
{
for (var i = 0; i < sampleCount; i++)
{
Expand All @@ -90,9 +102,10 @@ private static void CreateRevenueGauge(int sampleCount)
}
}

private static void MeasureShrimp(int sampleCount)
private static void MeasureShrimp()
{
using (new Timing(nameof(MeasureShrimp), MeasurementUnit.Duration.Millisecond))
const int sampleCount = 30;
using (SentrySdk.Metrics.StartTimer(nameof(MeasureShrimp), MeasurementUnit.Duration.Millisecond))
{
for (var i = 0; i < sampleCount; i++)
{
Expand All @@ -102,4 +115,25 @@ private static void MeasureShrimp(int sampleCount)
}
}
}

private static void SellHats()
{
// Here we're emitting the metric using System.Diagnostics.Metrics instead of SentrySdk.Metrics.
// We won't see accurate code locations for these, so Sentry.Metrics are preferable but support
// for System.Diagnostics.Metrics means Sentry can collect a bunch built in metrics without you
// having to instrument anything... see case 4 below
HatsSold.Add(Roll.Next(0, 1000));
}

private static async Task CallSampleApiAsync()
{
// Here we demonstrate collecting some built in metrics for HTTP requests... this works because
// we've configured ExperimentalMetricsOptions.CaptureInstruments to match "http.client.*"
//
// See https://learn.microsoft.com/en-us/dotnet/core/diagnostics/built-in-metrics-system-net#systemnethttp
var httpClient = new HttpClient();
var url = "https://api.sampleapis.com/coffee/hot";
var result = await httpClient.GetAsync(url);
System.Console.WriteLine($"GET {url} {result.StatusCode}");
}
}
173 changes: 173 additions & 0 deletions src/Sentry/BuiltInSystemDiagnosticsMeters.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
namespace Sentry;

/// <summary>
/// Well known values for built in metrics that can be configured for
/// <see cref="ExperimentalMetricsOptions.CaptureSystemDiagnosticsMeters"/>
/// </summary>
public static partial class BuiltInSystemDiagnosticsMeters
{
private const string MicrosoftAspNetCoreHostingPattern = @"^Microsoft\.AspNetCore\.Hosting$";
private const string MicrosoftAspNetCoreRoutingPattern = @"^Microsoft\.AspNetCore\.Routing$";
private const string MicrosoftAspNetCoreDiagnosticsPattern = @"^Microsoft\.AspNetCore\.Diagnostics$";
private const string MicrosoftAspNetCoreRateLimitingPattern = @"^Microsoft\.AspNetCore\.RateLimiting$";
private const string MicrosoftAspNetCoreHeaderParsingPattern = @"^Microsoft\.AspNetCore\.HeaderParsing$";
private const string MicrosoftAspNetCoreServerKestrelPattern = @"^Microsoft\.AspNetCore\.Server\.Kestrel$";
private const string MicrosoftAspNetCoreHttpConnectionsPattern = @"^Microsoft\.AspNetCore\.Http\.Connections$";
private const string MicrosoftExtensionsDiagnosticsHealthChecksPattern = @"^Microsoft\.Extensions\.Diagnostics\.HealthChecks$";
private const string MicrosoftExtensionsDiagnosticsResourceMonitoringPattern = @"^Microsoft\.Extensions\.Diagnostics\.ResourceMonitoring$";
private const string SystemNetNameResolutionPattern = @"^System\.Net\.NameResolution$";
private const string SystemNetHttpPattern = @"^System\.Net\.Http$";

/// <summary>
/// Matches the built in Microsoft.AspNetCore.Hosting metrics
/// </summary>
#if NET8_0_OR_GREATER
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreHosting = MicrosoftAspNetCoreHostingRegex();

[GeneratedRegex(MicrosoftAspNetCoreHostingPattern, RegexOptions.Compiled)]
private static partial Regex MicrosoftAspNetCoreHostingRegex();
#else
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreHosting = new Regex(MicrosoftAspNetCoreHostingPattern, RegexOptions.Compiled);
#endif

/// <summary>
/// Matches the built in Microsoft.AspNetCore.Routing metrics
/// </summary>
#if NET8_0_OR_GREATER
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreRouting = MicrosoftAspNetCoreRoutingRegex();

[GeneratedRegex(MicrosoftAspNetCoreRoutingPattern, RegexOptions.Compiled)]
private static partial Regex MicrosoftAspNetCoreRoutingRegex();
#else
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreRouting = new Regex(MicrosoftAspNetCoreRoutingPattern, RegexOptions.Compiled);
#endif

/// <summary>
/// Matches the built in Microsoft.AspNetCore.Diagnostics metrics
/// </summary>
#if NET8_0_OR_GREATER
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreDiagnostics = MicrosoftAspNetCoreDiagnosticsRegex();

[GeneratedRegex(MicrosoftAspNetCoreDiagnosticsPattern, RegexOptions.Compiled)]
private static partial Regex MicrosoftAspNetCoreDiagnosticsRegex();
#else
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreDiagnostics = new Regex(MicrosoftAspNetCoreDiagnosticsPattern, RegexOptions.Compiled);
#endif

/// <summary>
/// Matches the built in Microsoft.AspNetCore.RateLimiting metrics
/// </summary>
#if NET8_0_OR_GREATER
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreRateLimiting = MicrosoftAspNetCoreRateLimitingRegex();

[GeneratedRegex(MicrosoftAspNetCoreRateLimitingPattern, RegexOptions.Compiled)]
private static partial Regex MicrosoftAspNetCoreRateLimitingRegex();
#else
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreRateLimiting = new Regex(MicrosoftAspNetCoreRateLimitingPattern, RegexOptions.Compiled);
#endif

/// <summary>
/// Matches the built in Microsoft.AspNetCore.HeaderParsing metrics
/// </summary>
#if NET8_0_OR_GREATER
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreHeaderParsing = MicrosoftAspNetCoreHeaderParsingRegex();

[GeneratedRegex(MicrosoftAspNetCoreHeaderParsingPattern, RegexOptions.Compiled)]
private static partial Regex MicrosoftAspNetCoreHeaderParsingRegex();
#else
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreHeaderParsing = new Regex(MicrosoftAspNetCoreHeaderParsingPattern, RegexOptions.Compiled);
#endif

/// <summary>
/// Matches the built in Microsoft.AspNetCore.Server.Kestrel metrics
/// </summary>
#if NET8_0_OR_GREATER
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreServerKestrel = MicrosoftAspNetCoreServerKestrelRegex();

[GeneratedRegex(MicrosoftAspNetCoreServerKestrelPattern, RegexOptions.Compiled)]
private static partial Regex MicrosoftAspNetCoreServerKestrelRegex();
#else
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreServerKestrel = new Regex(MicrosoftAspNetCoreServerKestrelPattern, RegexOptions.Compiled);
#endif

/// <summary>
/// Matches the built in Microsoft.AspNetCore.Http.Connections metrics
/// </summary>
#if NET8_0_OR_GREATER
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreHttpConnections = MicrosoftAspNetCoreHttpConnectionsRegex();

[GeneratedRegex(MicrosoftAspNetCoreHttpConnectionsPattern, RegexOptions.Compiled)]
private static partial Regex MicrosoftAspNetCoreHttpConnectionsRegex();
#else
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreHttpConnections = new Regex(MicrosoftAspNetCoreHttpConnectionsPattern, RegexOptions.Compiled);
#endif

/// <summary>
/// Matches the built in Microsoft.Extensions.Diagnostics.HealthChecks metrics
/// </summary>
#if NET8_0_OR_GREATER
public static readonly SubstringOrRegexPattern MicrosoftExtensionsDiagnosticsHealthChecks = MicrosoftExtensionsDiagnosticsHealthChecksRegex();

[GeneratedRegex(MicrosoftExtensionsDiagnosticsHealthChecksPattern, RegexOptions.Compiled)]
private static partial Regex MicrosoftExtensionsDiagnosticsHealthChecksRegex();
#else
public static readonly SubstringOrRegexPattern MicrosoftExtensionsDiagnosticsHealthChecks = new Regex(MicrosoftExtensionsDiagnosticsHealthChecksPattern, RegexOptions.Compiled);
#endif

/// <summary>
/// Matches the built in Microsoft.Extensions.Diagnostics.ResourceMonitoring metrics
/// </summary>
#if NET8_0_OR_GREATER
public static readonly SubstringOrRegexPattern MicrosoftExtensionsDiagnosticsResourceMonitoring = MicrosoftExtensionsDiagnosticsResourceMonitoringRegex();

[GeneratedRegex(MicrosoftExtensionsDiagnosticsResourceMonitoringPattern, RegexOptions.Compiled)]
private static partial Regex MicrosoftExtensionsDiagnosticsResourceMonitoringRegex();
#else
public static readonly SubstringOrRegexPattern MicrosoftExtensionsDiagnosticsResourceMonitoring = new Regex(MicrosoftExtensionsDiagnosticsResourceMonitoringPattern, RegexOptions.Compiled);
#endif

/// <summary>
/// Matches the built in System.Net.NameResolution metrics
/// </summary>
#if NET8_0_OR_GREATER
public static readonly SubstringOrRegexPattern SystemNetNameResolution = SystemNetNameResolutionRegex();

[GeneratedRegex(SystemNetNameResolutionPattern, RegexOptions.Compiled)]
private static partial Regex SystemNetNameResolutionRegex();
#else
public static readonly SubstringOrRegexPattern SystemNetNameResolution = new Regex(SystemNetNameResolutionPattern, RegexOptions.Compiled);
#endif

/// <summary>
/// Matches the built in <see cref="System.Net.Http"/> metrics
/// </summary>
#if NET8_0_OR_GREATER
public static readonly SubstringOrRegexPattern SystemNetHttp = SystemNetHttpRegex();

[GeneratedRegex(SystemNetHttpPattern, RegexOptions.Compiled)]
private static partial Regex SystemNetHttpRegex();
#else
public static readonly SubstringOrRegexPattern SystemNetHttp = new Regex(SystemNetHttpPattern, RegexOptions.Compiled);
#endif

private static readonly Lazy<IList<SubstringOrRegexPattern>> LazyAll = new(() => new List<SubstringOrRegexPattern>
{
MicrosoftAspNetCoreHosting,
MicrosoftAspNetCoreRouting,
MicrosoftAspNetCoreDiagnostics,
MicrosoftAspNetCoreRateLimiting,
MicrosoftAspNetCoreHeaderParsing,
MicrosoftAspNetCoreServerKestrel,
MicrosoftAspNetCoreHttpConnections,
SystemNetNameResolution,
SystemNetHttp,
MicrosoftExtensionsDiagnosticsHealthChecks,
MicrosoftExtensionsDiagnosticsResourceMonitoring
});

/// <summary>
/// Matches all built in metrics
/// </summary>
/// <returns></returns>
public static IList<SubstringOrRegexPattern> All => LazyAll.Value;
}
19 changes: 19 additions & 0 deletions src/Sentry/DisabledMetricAggregator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,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 @@ -55,3 +63,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
}
}
Loading

0 comments on commit b832505

Please sign in to comment.