Skip to content

Commit d095eac

Browse files
bruno-garciaclaude
andauthored
feat: upgrade Sentry SDK to 6.1.0-alpha.1 and add trace-connected met… (#252)
* feat: upgrade Sentry SDK to 6.1.0-alpha.1 and add trace-connected metrics Upgrades Sentry .NET SDK from 6.0.0 to 6.1.0-alpha.1 to test the new experimental trace-connected metrics API from PR getsentry/sentry-dotnet#4834. Changes: - Upgrade Sentry SDK to 6.1.0-alpha.1 - Add SentryClientMetrics decorator that emits metrics to Sentry - Make ClientMetrics methods virtual for proper polymorphism - Enable experimental metrics in all apps (Server, Console, Android) - Use SentryClientMetrics in DI registrations (Core, Server, Console) - Fix duplicate ClientMetrics registration in Core/Startup.cs The new metrics API provides: - AddCounter: for counting events (files, uploads, errors) - RecordDistribution: for value distributions (uploaded bytes) - RecordGauge: for point-in-time values (jobs in flight) Metrics are automatically correlated with the active trace/span. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test: add unit tests for SentryClientMetrics Adds tests to verify that: - SentryClientMetrics emits trace_metric items to Sentry - All metric types (Counter, Distribution, Gauge) are emitted - Base class counters are also incremented (dual emission) - Polymorphism works correctly (override vs new) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: use IHub for SentryClientMetrics to improve testability - SentryClientMetrics now accepts IHub in constructor (defaults to HubAdapter.Instance) - Tests use isolated SDK instances with recording transport - Each test initializes/disposes its own SDK for proper isolation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Delete test/SymbolCollector.Core.Tests/SentryClientMetricsTests.cs --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 0da31e9 commit d095eac

File tree

8 files changed

+194
-19
lines changed

8 files changed

+194
-19
lines changed

src/Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project>
22
<PropertyGroup>
33
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
4-
<SentryVersion>6.0.0</SentryVersion>
4+
<SentryVersion>6.1.0-alpha.1</SentryVersion>
55
</PropertyGroup>
66
<ItemGroup>
77
<PackageVersion Include="Sentry" Version="$(SentryVersion)" />

src/SymbolCollector.Android.Library/Host.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public static IHost Init(Context context, string dsn, string? sentryTrace = null
4040
{
4141
o.CaptureFailedRequests = true;
4242
o.EnableLogs = true;
43+
o.Experimental.EnableMetrics = true;
4344

4445
o.SetBeforeSend(@event =>
4546
{

src/SymbolCollector.Console/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace SymbolCollector.Console;
1313

1414
internal class Program
1515
{
16-
private static readonly ClientMetrics Metrics = new ClientMetrics();
16+
private static readonly SentryClientMetrics Metrics = new SentryClientMetrics();
1717

1818
static async Task<int> Main(
1919
string? upload = null,
@@ -221,6 +221,7 @@ private static void Bootstrap(Args args)
221221
o.IsGlobalModeEnabled = true;
222222
o.CaptureFailedRequests = true;
223223
o.EnableLogs = true;
224+
o.Experimental.EnableMetrics = true;
224225

225226
#if DEBUG
226227
o.Environment = "development";

src/SymbolCollector.Core/ClientMetrics.cs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,72 +40,72 @@ public class ClientMetrics : IClientMetrics
4040
public int DirectoryDoesNotExistCount => _directoryDoesNotExistCount;
4141
public int FileDoesNotExistCount => _fileDoesNotExistCount;
4242

43-
public void FileProcessed()
43+
public virtual void FileProcessed()
4444
{
4545
Interlocked.Increment(ref _filesProcessedCount);
4646
}
4747

48-
public void MachOFileFound()
48+
public virtual void MachOFileFound()
4949
{
5050
Interlocked.Increment(ref _machOFileFoundCount);
5151
}
5252

53-
public void ElfFileFound()
53+
public virtual void ElfFileFound()
5454
{
5555
Interlocked.Increment(ref _elfFileFoundCount);
5656
}
5757

58-
public void FatMachOFileFound()
58+
public virtual void FatMachOFileFound()
5959
{
6060
Interlocked.Increment(ref _fatMachOFileFoundCount);
6161
}
6262

63-
public void FailedToUpload()
63+
public virtual void FailedToUpload()
6464
{
6565
Interlocked.Increment(ref _failedToUploadCount);
6666
}
6767

68-
public void FailedToParse()
68+
public virtual void FailedToParse()
6969
{
7070
Interlocked.Increment(ref _failedToParse);
7171
}
7272

73-
public void SuccessfulUpload()
73+
public virtual void SuccessfulUpload()
7474
{
7575
Interlocked.Increment(ref _successfullyUploadCount);
7676
}
7777

78-
public void AlreadyExisted()
78+
public virtual void AlreadyExisted()
7979
{
8080
Interlocked.Increment(ref _alreadyExistedCount);
8181
}
8282

83-
public void JobsInFlightRemove(int tasksCount)
83+
public virtual void JobsInFlightRemove(int tasksCount)
8484
{
8585
Interlocked.Add(ref _jobsInFlightCount, -tasksCount);
8686
}
8787

88-
public void JobsInFlightAdd(int tasksCount)
88+
public virtual void JobsInFlightAdd(int tasksCount)
8989
{
9090
Interlocked.Add(ref _jobsInFlightCount, tasksCount);
9191
}
9292

93-
public void UploadedBytesAdd(long bytes)
93+
public virtual void UploadedBytesAdd(long bytes)
9494
{
9595
Interlocked.Add(ref _uploadedBytesCount, bytes);
9696
}
9797

98-
public void FileOrDirectoryUnauthorizedAccess()
98+
public virtual void FileOrDirectoryUnauthorizedAccess()
9999
{
100100
Interlocked.Increment(ref _fileOrDirectoryUnauthorizedAccessCount);
101101
}
102102

103-
public void DirectoryDoesNotExist()
103+
public virtual void DirectoryDoesNotExist()
104104
{
105105
Interlocked.Increment(ref _directoryDoesNotExistCount);
106106
}
107107

108-
public void FileDoesNotExist()
108+
public virtual void FileDoesNotExist()
109109
{
110110
Interlocked.Increment(ref _fileDoesNotExistCount);
111111
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
#pragma warning disable SENTRYTRACECONNECTEDMETRICS // IHub.Metrics is experimental
2+
using Sentry;
3+
using Sentry.Extensibility;
4+
5+
namespace SymbolCollector.Core;
6+
7+
/// <summary>
8+
/// A decorator for <see cref="ClientMetrics"/> that also emits metrics to Sentry's trace-connected metrics API.
9+
/// </summary>
10+
/// <remarks>
11+
/// This integrates with Sentry SDK 6.1.0's new experimental trace-connected metrics feature.
12+
/// Metrics are attached to the current trace/span for correlation in Sentry's UI.
13+
/// Enable with <c>options.Experimental.EnableMetrics = true</c>.
14+
/// </remarks>
15+
public class SentryClientMetrics : ClientMetrics
16+
{
17+
private readonly IHub _hub;
18+
19+
/// <summary>
20+
/// Creates a new instance using the default <see cref="HubAdapter.Instance"/>.
21+
/// </summary>
22+
public SentryClientMetrics() : this(HubAdapter.Instance)
23+
{
24+
}
25+
26+
/// <summary>
27+
/// Creates a new instance with the specified hub for metrics emission.
28+
/// </summary>
29+
/// <param name="hub">The Sentry hub to use for emitting metrics.</param>
30+
public SentryClientMetrics(IHub hub)
31+
{
32+
_hub = hub;
33+
}
34+
35+
private SentryTraceMetrics Metrics => _hub.Metrics;
36+
37+
/// <summary>
38+
/// Records a file processed event, incrementing both local and Sentry counters.
39+
/// </summary>
40+
public override void FileProcessed()
41+
{
42+
base.FileProcessed();
43+
Metrics.AddCounter("symbol_collector.files_processed", 1);
44+
}
45+
46+
/// <summary>
47+
/// Records a Mach-O file discovery.
48+
/// </summary>
49+
public override void MachOFileFound()
50+
{
51+
base.MachOFileFound();
52+
Metrics.AddCounter("symbol_collector.debug_images_found", 1,
53+
[new KeyValuePair<string, object>("format", "macho")]);
54+
}
55+
56+
/// <summary>
57+
/// Records an ELF file discovery.
58+
/// </summary>
59+
public override void ElfFileFound()
60+
{
61+
base.ElfFileFound();
62+
Metrics.AddCounter("symbol_collector.debug_images_found", 1,
63+
[new KeyValuePair<string, object>("format", "elf")]);
64+
}
65+
66+
/// <summary>
67+
/// Records a Fat Mach-O file discovery.
68+
/// </summary>
69+
public override void FatMachOFileFound()
70+
{
71+
base.FatMachOFileFound();
72+
Metrics.AddCounter("symbol_collector.debug_images_found", 1,
73+
[new KeyValuePair<string, object>("format", "fat_macho")]);
74+
}
75+
76+
/// <summary>
77+
/// Records a failed upload.
78+
/// </summary>
79+
public override void FailedToUpload()
80+
{
81+
base.FailedToUpload();
82+
Metrics.AddCounter("symbol_collector.uploads", 1,
83+
[new KeyValuePair<string, object>("status", "failed")]);
84+
}
85+
86+
/// <summary>
87+
/// Records a parse failure.
88+
/// </summary>
89+
public override void FailedToParse()
90+
{
91+
base.FailedToParse();
92+
Metrics.AddCounter("symbol_collector.parse_failures", 1);
93+
}
94+
95+
/// <summary>
96+
/// Records a successful upload.
97+
/// </summary>
98+
public override void SuccessfulUpload()
99+
{
100+
base.SuccessfulUpload();
101+
Metrics.AddCounter("symbol_collector.uploads", 1,
102+
[new KeyValuePair<string, object>("status", "success")]);
103+
}
104+
105+
/// <summary>
106+
/// Records when a file already existed on the server.
107+
/// </summary>
108+
public override void AlreadyExisted()
109+
{
110+
base.AlreadyExisted();
111+
Metrics.AddCounter("symbol_collector.uploads", 1,
112+
[new KeyValuePair<string, object>("status", "already_exists")]);
113+
}
114+
115+
/// <summary>
116+
/// Removes jobs from the in-flight count.
117+
/// </summary>
118+
public override void JobsInFlightRemove(int tasksCount)
119+
{
120+
base.JobsInFlightRemove(tasksCount);
121+
Metrics.RecordGauge("symbol_collector.jobs_in_flight", JobsInFlightCount);
122+
}
123+
124+
/// <summary>
125+
/// Adds jobs to the in-flight count.
126+
/// </summary>
127+
public override void JobsInFlightAdd(int tasksCount)
128+
{
129+
base.JobsInFlightAdd(tasksCount);
130+
Metrics.RecordGauge("symbol_collector.jobs_in_flight", JobsInFlightCount);
131+
}
132+
133+
/// <summary>
134+
/// Records bytes uploaded, emitting as a distribution for percentile analysis.
135+
/// </summary>
136+
public override void UploadedBytesAdd(long bytes)
137+
{
138+
base.UploadedBytesAdd(bytes);
139+
Metrics.RecordDistribution("symbol_collector.uploaded_bytes", bytes, "byte");
140+
}
141+
142+
/// <summary>
143+
/// Records an unauthorized access error.
144+
/// </summary>
145+
public override void FileOrDirectoryUnauthorizedAccess()
146+
{
147+
base.FileOrDirectoryUnauthorizedAccess();
148+
Metrics.AddCounter("symbol_collector.access_errors", 1,
149+
[new KeyValuePair<string, object>("type", "unauthorized")]);
150+
}
151+
152+
/// <summary>
153+
/// Records a directory not found error.
154+
/// </summary>
155+
public override void DirectoryDoesNotExist()
156+
{
157+
base.DirectoryDoesNotExist();
158+
Metrics.AddCounter("symbol_collector.access_errors", 1,
159+
[new KeyValuePair<string, object>("type", "directory_not_found")]);
160+
}
161+
162+
/// <summary>
163+
/// Records a file not found error.
164+
/// </summary>
165+
public override void FileDoesNotExist()
166+
{
167+
base.FileDoesNotExist();
168+
Metrics.AddCounter("symbol_collector.access_errors", 1,
169+
[new KeyValuePair<string, object>("type", "file_not_found")]);
170+
}
171+
}

src/SymbolCollector.Core/Startup.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ private static void ConfigureServices(IServiceCollection services)
4848
{
4949
services.AddSingleton<Client>();
5050
services.AddSingleton<ObjectFileParser>();
51-
services.AddSingleton<ClientMetrics>();
51+
services.AddSingleton<SentryClientMetrics>();
52+
services.AddSingleton<ClientMetrics>(sp => sp.GetRequiredService<SentryClientMetrics>());
5253
services.AddSingleton<FatBinaryReader>();
53-
services.AddSingleton<ClientMetrics>();
5454

5555
services.AddOptions<SymbolClientOptions>()
5656
.Configure<IConfiguration>((o, f) => f.Bind("SymbolClient", o))

src/SymbolCollector.Server/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ private static IHostBuilder CreateHostBuilder(string[] args) =>
7575
o.MinimumBreadcrumbLevel = LogLevel.Debug;
7676
o.CaptureFailedRequests = true;
7777
o.EnableLogs = true;
78+
o.Experimental.EnableMetrics = true;
7879

7980
// https://github.com/getsentry/symbol-collector/issues/205
8081
// o.CaptureBlockingCalls = true;

src/SymbolCollector.Server/Startup.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ public void ConfigureServices(IServiceCollection services)
2424

2525
services.AddSingleton<ObjectFileParser>();
2626
services.AddSingleton<FatBinaryReader>();
27-
services.AddSingleton<ClientMetrics>();
27+
services.AddSingleton<SentryClientMetrics>();
28+
services.AddSingleton<ClientMetrics>(sp => sp.GetRequiredService<SentryClientMetrics>());
2829
services.AddSingleton<IBatchFinalizer, SymsorterBatchFinalizer>();
2930
services.AddSingleton<ISymbolGcsWriter, SymbolGcsWriter>();
3031
services.AddSingleton<IStorageClientFactory, StorageClientFactory>();

0 commit comments

Comments
 (0)