Skip to content

Commit 467676e

Browse files
Capture built in metrics from System.Diagnostics.Metrics API (#3052)
1 parent 741e088 commit 467676e

File tree

33 files changed

+1594
-42
lines changed

33 files changed

+1594
-42
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Features
6+
7+
- Added support for capturing built in metrics from the System.Diagnostics.Metrics API ([#3052](https://github.com/getsentry/sentry-dotnet/pull/3052))
8+
59
### Significant change in behavior
610

711
- Added `Sentry` namespace to global usings when `ImplicitUsings` is enabled ([#3043](https://github.com/getsentry/sentry-dotnet/pull/3043))

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

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
1-
namespace Sentry.Samples.Console.Metrics;
1+
using System.Diagnostics.Metrics;
2+
using System.Text.RegularExpressions;
3+
4+
namespace Sentry.Samples.Console.Metrics;
25

36
internal static class Program
47
{
5-
private static readonly Random Roll = new();
8+
private static readonly Random Roll = Random.Shared;
9+
10+
// Sentry also supports capturing System.Diagnostics.Metrics
11+
private static readonly Meter HatsMeter = new("HatCo.HatStore", "1.0.0");
12+
private static readonly Counter<int> HatsSold = HatsMeter.CreateCounter<int>(
13+
name: "hats-sold",
14+
unit: "Hats",
15+
description: "The number of hats sold in our store");
616

7-
private static void Main()
17+
private static async Task Main()
818
{
919
// Enable the SDK
1020
using (SentrySdk.Init(options =>
@@ -20,41 +30,44 @@ private static void Main()
2030
// Initialize some (non null) ExperimentalMetricsOptions to enable Sentry Metrics,
2131
options.ExperimentalMetrics = new ExperimentalMetricsOptions
2232
{
23-
EnableCodeLocations =
24-
true // Set this to false if you don't want to track code locations for some reason
33+
EnableCodeLocations = true, // Set this to false if you don't want to track code locations
34+
CaptureSystemDiagnosticsInstruments = [
35+
// Capture System.Diagnostics.Metrics matching the name "HatCo.HatStore", which is the name
36+
// of the custom HatsMeter defined above
37+
"hats-sold"
38+
],
39+
// Capture all built in metrics (this is the default - you can override this to capture some or
40+
// none of these if you prefer)
41+
CaptureSystemDiagnosticsMeters = BuiltInSystemDiagnosticsMeters.All
2542
};
2643
}))
2744
{
2845
System.Console.WriteLine("Measure, Yeah, Measure!");
29-
Action[] actions =
30-
[
31-
() => PlaySetBingo(10),
32-
() => CreateRevenueGauge(100),
33-
() => MeasureShrimp(30),
34-
];
35-
while (true)
46+
47+
Action[] actions = [PlaySetBingo, CreateRevenueGauge, MeasureShrimp, SellHats];
48+
do
3649
{
37-
// Perform your task here
38-
var actionIdx = Roll.Next(0, actions.Length);
39-
actions[actionIdx]();
50+
// Run a random action
51+
var idx = Roll.Next(0, actions.Length);
52+
actions[idx]();
53+
54+
// Make an API call
55+
await CallSampleApiAsync();
4056

4157
// Optional: Delay to prevent tight looping
42-
var sleepTime = Roll.Next(1, 10);
58+
var sleepTime = Roll.Next(1, 5);
4359
System.Console.WriteLine($"Sleeping for {sleepTime} second(s).");
4460
System.Console.WriteLine("Press any key to stop...");
4561
Thread.Sleep(TimeSpan.FromSeconds(sleepTime));
46-
// Check if a key has been pressed
47-
if (System.Console.KeyAvailable)
48-
{
49-
break;
50-
}
5162
}
63+
while (!System.Console.KeyAvailable);
5264
System.Console.WriteLine("Measure up");
5365
}
5466
}
5567

56-
private static void PlaySetBingo(int attempts)
68+
private static void PlaySetBingo()
5769
{
70+
const int attempts = 10;
5871
var solution = new[] { 3, 5, 7, 11, 13, 17 };
5972

6073
// StartTimer creates a distribution that is designed to measure the amount of time it takes to run code
@@ -74,8 +87,9 @@ private static void PlaySetBingo(int attempts)
7487
}
7588
}
7689

77-
private static void CreateRevenueGauge(int sampleCount)
90+
private static void CreateRevenueGauge()
7891
{
92+
const int sampleCount = 100;
7993
using (SentrySdk.Metrics.StartTimer(nameof(CreateRevenueGauge), MeasurementUnit.Duration.Millisecond))
8094
{
8195
for (var i = 0; i < sampleCount; i++)
@@ -88,8 +102,9 @@ private static void CreateRevenueGauge(int sampleCount)
88102
}
89103
}
90104

91-
private static void MeasureShrimp(int sampleCount)
105+
private static void MeasureShrimp()
92106
{
107+
const int sampleCount = 30;
93108
using (SentrySdk.Metrics.StartTimer(nameof(MeasureShrimp), MeasurementUnit.Duration.Millisecond))
94109
{
95110
for (var i = 0; i < sampleCount; i++)
@@ -100,4 +115,25 @@ private static void MeasureShrimp(int sampleCount)
100115
}
101116
}
102117
}
118+
119+
private static void SellHats()
120+
{
121+
// Here we're emitting the metric using System.Diagnostics.Metrics instead of SentrySdk.Metrics.
122+
// We won't see accurate code locations for these, so Sentry.Metrics are preferable but support
123+
// for System.Diagnostics.Metrics means Sentry can collect a bunch built in metrics without you
124+
// having to instrument anything... see case 4 below
125+
HatsSold.Add(Roll.Next(0, 1000));
126+
}
127+
128+
private static async Task CallSampleApiAsync()
129+
{
130+
// Here we demonstrate collecting some built in metrics for HTTP requests... this works because
131+
// we've configured ExperimentalMetricsOptions.CaptureInstruments to match "http.client.*"
132+
//
133+
// See https://learn.microsoft.com/en-us/dotnet/core/diagnostics/built-in-metrics-system-net#systemnethttp
134+
var httpClient = new HttpClient();
135+
var url = "https://api.sampleapis.com/coffee/hot";
136+
var result = await httpClient.GetAsync(url);
137+
System.Console.WriteLine($"GET {url} {result.StatusCode}");
138+
}
103139
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
namespace Sentry;
2+
3+
/// <summary>
4+
/// Well known values for built in metrics that can be configured for
5+
/// <see cref="ExperimentalMetricsOptions.CaptureSystemDiagnosticsMeters"/>
6+
/// </summary>
7+
public static partial class BuiltInSystemDiagnosticsMeters
8+
{
9+
private const string MicrosoftAspNetCoreHostingPattern = @"^Microsoft\.AspNetCore\.Hosting$";
10+
private const string MicrosoftAspNetCoreRoutingPattern = @"^Microsoft\.AspNetCore\.Routing$";
11+
private const string MicrosoftAspNetCoreDiagnosticsPattern = @"^Microsoft\.AspNetCore\.Diagnostics$";
12+
private const string MicrosoftAspNetCoreRateLimitingPattern = @"^Microsoft\.AspNetCore\.RateLimiting$";
13+
private const string MicrosoftAspNetCoreHeaderParsingPattern = @"^Microsoft\.AspNetCore\.HeaderParsing$";
14+
private const string MicrosoftAspNetCoreServerKestrelPattern = @"^Microsoft\.AspNetCore\.Server\.Kestrel$";
15+
private const string MicrosoftAspNetCoreHttpConnectionsPattern = @"^Microsoft\.AspNetCore\.Http\.Connections$";
16+
private const string MicrosoftExtensionsDiagnosticsHealthChecksPattern = @"^Microsoft\.Extensions\.Diagnostics\.HealthChecks$";
17+
private const string MicrosoftExtensionsDiagnosticsResourceMonitoringPattern = @"^Microsoft\.Extensions\.Diagnostics\.ResourceMonitoring$";
18+
private const string SystemNetNameResolutionPattern = @"^System\.Net\.NameResolution$";
19+
private const string SystemNetHttpPattern = @"^System\.Net\.Http$";
20+
21+
/// <summary>
22+
/// Matches the built in Microsoft.AspNetCore.Hosting metrics
23+
/// </summary>
24+
#if NET8_0_OR_GREATER
25+
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreHosting = MicrosoftAspNetCoreHostingRegex();
26+
27+
[GeneratedRegex(MicrosoftAspNetCoreHostingPattern, RegexOptions.Compiled)]
28+
private static partial Regex MicrosoftAspNetCoreHostingRegex();
29+
#else
30+
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreHosting = new Regex(MicrosoftAspNetCoreHostingPattern, RegexOptions.Compiled);
31+
#endif
32+
33+
/// <summary>
34+
/// Matches the built in Microsoft.AspNetCore.Routing metrics
35+
/// </summary>
36+
#if NET8_0_OR_GREATER
37+
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreRouting = MicrosoftAspNetCoreRoutingRegex();
38+
39+
[GeneratedRegex(MicrosoftAspNetCoreRoutingPattern, RegexOptions.Compiled)]
40+
private static partial Regex MicrosoftAspNetCoreRoutingRegex();
41+
#else
42+
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreRouting = new Regex(MicrosoftAspNetCoreRoutingPattern, RegexOptions.Compiled);
43+
#endif
44+
45+
/// <summary>
46+
/// Matches the built in Microsoft.AspNetCore.Diagnostics metrics
47+
/// </summary>
48+
#if NET8_0_OR_GREATER
49+
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreDiagnostics = MicrosoftAspNetCoreDiagnosticsRegex();
50+
51+
[GeneratedRegex(MicrosoftAspNetCoreDiagnosticsPattern, RegexOptions.Compiled)]
52+
private static partial Regex MicrosoftAspNetCoreDiagnosticsRegex();
53+
#else
54+
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreDiagnostics = new Regex(MicrosoftAspNetCoreDiagnosticsPattern, RegexOptions.Compiled);
55+
#endif
56+
57+
/// <summary>
58+
/// Matches the built in Microsoft.AspNetCore.RateLimiting metrics
59+
/// </summary>
60+
#if NET8_0_OR_GREATER
61+
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreRateLimiting = MicrosoftAspNetCoreRateLimitingRegex();
62+
63+
[GeneratedRegex(MicrosoftAspNetCoreRateLimitingPattern, RegexOptions.Compiled)]
64+
private static partial Regex MicrosoftAspNetCoreRateLimitingRegex();
65+
#else
66+
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreRateLimiting = new Regex(MicrosoftAspNetCoreRateLimitingPattern, RegexOptions.Compiled);
67+
#endif
68+
69+
/// <summary>
70+
/// Matches the built in Microsoft.AspNetCore.HeaderParsing metrics
71+
/// </summary>
72+
#if NET8_0_OR_GREATER
73+
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreHeaderParsing = MicrosoftAspNetCoreHeaderParsingRegex();
74+
75+
[GeneratedRegex(MicrosoftAspNetCoreHeaderParsingPattern, RegexOptions.Compiled)]
76+
private static partial Regex MicrosoftAspNetCoreHeaderParsingRegex();
77+
#else
78+
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreHeaderParsing = new Regex(MicrosoftAspNetCoreHeaderParsingPattern, RegexOptions.Compiled);
79+
#endif
80+
81+
/// <summary>
82+
/// Matches the built in Microsoft.AspNetCore.Server.Kestrel metrics
83+
/// </summary>
84+
#if NET8_0_OR_GREATER
85+
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreServerKestrel = MicrosoftAspNetCoreServerKestrelRegex();
86+
87+
[GeneratedRegex(MicrosoftAspNetCoreServerKestrelPattern, RegexOptions.Compiled)]
88+
private static partial Regex MicrosoftAspNetCoreServerKestrelRegex();
89+
#else
90+
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreServerKestrel = new Regex(MicrosoftAspNetCoreServerKestrelPattern, RegexOptions.Compiled);
91+
#endif
92+
93+
/// <summary>
94+
/// Matches the built in Microsoft.AspNetCore.Http.Connections metrics
95+
/// </summary>
96+
#if NET8_0_OR_GREATER
97+
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreHttpConnections = MicrosoftAspNetCoreHttpConnectionsRegex();
98+
99+
[GeneratedRegex(MicrosoftAspNetCoreHttpConnectionsPattern, RegexOptions.Compiled)]
100+
private static partial Regex MicrosoftAspNetCoreHttpConnectionsRegex();
101+
#else
102+
public static readonly SubstringOrRegexPattern MicrosoftAspNetCoreHttpConnections = new Regex(MicrosoftAspNetCoreHttpConnectionsPattern, RegexOptions.Compiled);
103+
#endif
104+
105+
/// <summary>
106+
/// Matches the built in Microsoft.Extensions.Diagnostics.HealthChecks metrics
107+
/// </summary>
108+
#if NET8_0_OR_GREATER
109+
public static readonly SubstringOrRegexPattern MicrosoftExtensionsDiagnosticsHealthChecks = MicrosoftExtensionsDiagnosticsHealthChecksRegex();
110+
111+
[GeneratedRegex(MicrosoftExtensionsDiagnosticsHealthChecksPattern, RegexOptions.Compiled)]
112+
private static partial Regex MicrosoftExtensionsDiagnosticsHealthChecksRegex();
113+
#else
114+
public static readonly SubstringOrRegexPattern MicrosoftExtensionsDiagnosticsHealthChecks = new Regex(MicrosoftExtensionsDiagnosticsHealthChecksPattern, RegexOptions.Compiled);
115+
#endif
116+
117+
/// <summary>
118+
/// Matches the built in Microsoft.Extensions.Diagnostics.ResourceMonitoring metrics
119+
/// </summary>
120+
#if NET8_0_OR_GREATER
121+
public static readonly SubstringOrRegexPattern MicrosoftExtensionsDiagnosticsResourceMonitoring = MicrosoftExtensionsDiagnosticsResourceMonitoringRegex();
122+
123+
[GeneratedRegex(MicrosoftExtensionsDiagnosticsResourceMonitoringPattern, RegexOptions.Compiled)]
124+
private static partial Regex MicrosoftExtensionsDiagnosticsResourceMonitoringRegex();
125+
#else
126+
public static readonly SubstringOrRegexPattern MicrosoftExtensionsDiagnosticsResourceMonitoring = new Regex(MicrosoftExtensionsDiagnosticsResourceMonitoringPattern, RegexOptions.Compiled);
127+
#endif
128+
129+
/// <summary>
130+
/// Matches the built in System.Net.NameResolution metrics
131+
/// </summary>
132+
#if NET8_0_OR_GREATER
133+
public static readonly SubstringOrRegexPattern SystemNetNameResolution = SystemNetNameResolutionRegex();
134+
135+
[GeneratedRegex(SystemNetNameResolutionPattern, RegexOptions.Compiled)]
136+
private static partial Regex SystemNetNameResolutionRegex();
137+
#else
138+
public static readonly SubstringOrRegexPattern SystemNetNameResolution = new Regex(SystemNetNameResolutionPattern, RegexOptions.Compiled);
139+
#endif
140+
141+
/// <summary>
142+
/// Matches the built in <see cref="System.Net.Http"/> metrics
143+
/// </summary>
144+
#if NET8_0_OR_GREATER
145+
public static readonly SubstringOrRegexPattern SystemNetHttp = SystemNetHttpRegex();
146+
147+
[GeneratedRegex(SystemNetHttpPattern, RegexOptions.Compiled)]
148+
private static partial Regex SystemNetHttpRegex();
149+
#else
150+
public static readonly SubstringOrRegexPattern SystemNetHttp = new Regex(SystemNetHttpPattern, RegexOptions.Compiled);
151+
#endif
152+
153+
private static readonly Lazy<IList<SubstringOrRegexPattern>> LazyAll = new(() => new List<SubstringOrRegexPattern>
154+
{
155+
MicrosoftAspNetCoreHosting,
156+
MicrosoftAspNetCoreRouting,
157+
MicrosoftAspNetCoreDiagnostics,
158+
MicrosoftAspNetCoreRateLimiting,
159+
MicrosoftAspNetCoreHeaderParsing,
160+
MicrosoftAspNetCoreServerKestrel,
161+
MicrosoftAspNetCoreHttpConnections,
162+
SystemNetNameResolution,
163+
SystemNetHttp,
164+
MicrosoftExtensionsDiagnosticsHealthChecks,
165+
MicrosoftExtensionsDiagnosticsResourceMonitoring
166+
});
167+
168+
/// <summary>
169+
/// Matches all built in metrics
170+
/// </summary>
171+
/// <returns></returns>
172+
public static IList<SubstringOrRegexPattern> All => LazyAll.Value;
173+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
namespace Sentry;
2+
3+
/// <summary>
4+
/// Settings for the experimental Metrics feature. This feature is preview only and will very likely change in the future
5+
/// without a major version bump... so use at your own risk.
6+
/// </summary>
7+
public class ExperimentalMetricsOptions
8+
{
9+
/// <summary>
10+
/// Determines whether code locations should be recorded for Metrics
11+
/// </summary>
12+
public bool EnableCodeLocations { get; set; } = true;
13+
14+
private IList<SubstringOrRegexPattern> _captureSystemDiagnosticsInstruments = new List<SubstringOrRegexPattern>();
15+
16+
/// <summary>
17+
/// <para>
18+
/// A list of Substrings or Regular Expressions. Any `System.Diagnostics.Metrics.Instrument` whose name
19+
/// matches one of the items in this list will be collected and reported to Sentry.
20+
/// </para>
21+
/// <para>
22+
/// These can be either custom Instruments that you have created or any of the built in metrics that are available.
23+
/// </para>
24+
/// <para>
25+
/// See https://learn.microsoft.com/en-us/dotnet/core/diagnostics/built-in-metrics for more information.
26+
/// </para>
27+
/// </summary>
28+
public IList<SubstringOrRegexPattern> CaptureSystemDiagnosticsInstruments
29+
{
30+
// NOTE: During configuration binding, .NET 6 and lower used to just call Add on the existing item.
31+
// .NET 7 changed this to call the setter with an array that already starts with the old value.
32+
// We have to handle both cases.
33+
get => _captureSystemDiagnosticsInstruments;
34+
set => _captureSystemDiagnosticsInstruments = value.WithConfigBinding();
35+
}
36+
37+
private IList<SubstringOrRegexPattern> _captureSystemDiagnosticsMeters = BuiltInSystemDiagnosticsMeters.All;
38+
39+
/// <summary>
40+
/// <para>
41+
/// A list of Substrings or Regular Expressions. Instruments for any `System.Diagnostics.Metrics.Meter`
42+
/// whose name matches one of the items in this list will be collected and reported to Sentry.
43+
/// </para>
44+
/// <para>
45+
/// These can be either custom Instruments that you have created or any of the built in metrics that are available.
46+
/// </para>
47+
/// <para>
48+
/// See https://learn.microsoft.com/en-us/dotnet/core/diagnostics/built-in-metrics for more information.
49+
/// </para>
50+
/// </summary>
51+
public IList<SubstringOrRegexPattern> CaptureSystemDiagnosticsMeters
52+
{
53+
// NOTE: During configuration binding, .NET 6 and lower used to just call Add on the existing item.
54+
// .NET 7 changed this to call the setter with an array that already starts with the old value.
55+
// We have to handle both cases.
56+
get => _captureSystemDiagnosticsMeters;
57+
set => _captureSystemDiagnosticsMeters = value.WithConfigBinding();
58+
}
59+
}

0 commit comments

Comments
 (0)