diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 3ed6bbb..4f5b969 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -1,7 +1,7 @@ true - 6.0.0 + 6.1.0-alpha.1 diff --git a/src/SymbolCollector.Android.Library/Host.cs b/src/SymbolCollector.Android.Library/Host.cs index 471427d..b145672 100644 --- a/src/SymbolCollector.Android.Library/Host.cs +++ b/src/SymbolCollector.Android.Library/Host.cs @@ -40,6 +40,7 @@ public static IHost Init(Context context, string dsn, string? sentryTrace = null { o.CaptureFailedRequests = true; o.EnableLogs = true; + o.Experimental.EnableMetrics = true; o.SetBeforeSend(@event => { diff --git a/src/SymbolCollector.Console/Program.cs b/src/SymbolCollector.Console/Program.cs index 243e328..4356c96 100644 --- a/src/SymbolCollector.Console/Program.cs +++ b/src/SymbolCollector.Console/Program.cs @@ -13,7 +13,7 @@ namespace SymbolCollector.Console; internal class Program { - private static readonly ClientMetrics Metrics = new ClientMetrics(); + private static readonly SentryClientMetrics Metrics = new SentryClientMetrics(); static async Task Main( string? upload = null, @@ -221,6 +221,7 @@ private static void Bootstrap(Args args) o.IsGlobalModeEnabled = true; o.CaptureFailedRequests = true; o.EnableLogs = true; + o.Experimental.EnableMetrics = true; #if DEBUG o.Environment = "development"; diff --git a/src/SymbolCollector.Core/ClientMetrics.cs b/src/SymbolCollector.Core/ClientMetrics.cs index a86506d..1da08fd 100644 --- a/src/SymbolCollector.Core/ClientMetrics.cs +++ b/src/SymbolCollector.Core/ClientMetrics.cs @@ -40,72 +40,72 @@ public class ClientMetrics : IClientMetrics public int DirectoryDoesNotExistCount => _directoryDoesNotExistCount; public int FileDoesNotExistCount => _fileDoesNotExistCount; - public void FileProcessed() + public virtual void FileProcessed() { Interlocked.Increment(ref _filesProcessedCount); } - public void MachOFileFound() + public virtual void MachOFileFound() { Interlocked.Increment(ref _machOFileFoundCount); } - public void ElfFileFound() + public virtual void ElfFileFound() { Interlocked.Increment(ref _elfFileFoundCount); } - public void FatMachOFileFound() + public virtual void FatMachOFileFound() { Interlocked.Increment(ref _fatMachOFileFoundCount); } - public void FailedToUpload() + public virtual void FailedToUpload() { Interlocked.Increment(ref _failedToUploadCount); } - public void FailedToParse() + public virtual void FailedToParse() { Interlocked.Increment(ref _failedToParse); } - public void SuccessfulUpload() + public virtual void SuccessfulUpload() { Interlocked.Increment(ref _successfullyUploadCount); } - public void AlreadyExisted() + public virtual void AlreadyExisted() { Interlocked.Increment(ref _alreadyExistedCount); } - public void JobsInFlightRemove(int tasksCount) + public virtual void JobsInFlightRemove(int tasksCount) { Interlocked.Add(ref _jobsInFlightCount, -tasksCount); } - public void JobsInFlightAdd(int tasksCount) + public virtual void JobsInFlightAdd(int tasksCount) { Interlocked.Add(ref _jobsInFlightCount, tasksCount); } - public void UploadedBytesAdd(long bytes) + public virtual void UploadedBytesAdd(long bytes) { Interlocked.Add(ref _uploadedBytesCount, bytes); } - public void FileOrDirectoryUnauthorizedAccess() + public virtual void FileOrDirectoryUnauthorizedAccess() { Interlocked.Increment(ref _fileOrDirectoryUnauthorizedAccessCount); } - public void DirectoryDoesNotExist() + public virtual void DirectoryDoesNotExist() { Interlocked.Increment(ref _directoryDoesNotExistCount); } - public void FileDoesNotExist() + public virtual void FileDoesNotExist() { Interlocked.Increment(ref _fileDoesNotExistCount); } diff --git a/src/SymbolCollector.Core/SentryClientMetrics.cs b/src/SymbolCollector.Core/SentryClientMetrics.cs new file mode 100644 index 0000000..3aa4112 --- /dev/null +++ b/src/SymbolCollector.Core/SentryClientMetrics.cs @@ -0,0 +1,171 @@ +#pragma warning disable SENTRYTRACECONNECTEDMETRICS // IHub.Metrics is experimental +using Sentry; +using Sentry.Extensibility; + +namespace SymbolCollector.Core; + +/// +/// A decorator for that also emits metrics to Sentry's trace-connected metrics API. +/// +/// +/// This integrates with Sentry SDK 6.1.0's new experimental trace-connected metrics feature. +/// Metrics are attached to the current trace/span for correlation in Sentry's UI. +/// Enable with options.Experimental.EnableMetrics = true. +/// +public class SentryClientMetrics : ClientMetrics +{ + private readonly IHub _hub; + + /// + /// Creates a new instance using the default . + /// + public SentryClientMetrics() : this(HubAdapter.Instance) + { + } + + /// + /// Creates a new instance with the specified hub for metrics emission. + /// + /// The Sentry hub to use for emitting metrics. + public SentryClientMetrics(IHub hub) + { + _hub = hub; + } + + private SentryTraceMetrics Metrics => _hub.Metrics; + + /// + /// Records a file processed event, incrementing both local and Sentry counters. + /// + public override void FileProcessed() + { + base.FileProcessed(); + Metrics.AddCounter("symbol_collector.files_processed", 1); + } + + /// + /// Records a Mach-O file discovery. + /// + public override void MachOFileFound() + { + base.MachOFileFound(); + Metrics.AddCounter("symbol_collector.debug_images_found", 1, + [new KeyValuePair("format", "macho")]); + } + + /// + /// Records an ELF file discovery. + /// + public override void ElfFileFound() + { + base.ElfFileFound(); + Metrics.AddCounter("symbol_collector.debug_images_found", 1, + [new KeyValuePair("format", "elf")]); + } + + /// + /// Records a Fat Mach-O file discovery. + /// + public override void FatMachOFileFound() + { + base.FatMachOFileFound(); + Metrics.AddCounter("symbol_collector.debug_images_found", 1, + [new KeyValuePair("format", "fat_macho")]); + } + + /// + /// Records a failed upload. + /// + public override void FailedToUpload() + { + base.FailedToUpload(); + Metrics.AddCounter("symbol_collector.uploads", 1, + [new KeyValuePair("status", "failed")]); + } + + /// + /// Records a parse failure. + /// + public override void FailedToParse() + { + base.FailedToParse(); + Metrics.AddCounter("symbol_collector.parse_failures", 1); + } + + /// + /// Records a successful upload. + /// + public override void SuccessfulUpload() + { + base.SuccessfulUpload(); + Metrics.AddCounter("symbol_collector.uploads", 1, + [new KeyValuePair("status", "success")]); + } + + /// + /// Records when a file already existed on the server. + /// + public override void AlreadyExisted() + { + base.AlreadyExisted(); + Metrics.AddCounter("symbol_collector.uploads", 1, + [new KeyValuePair("status", "already_exists")]); + } + + /// + /// Removes jobs from the in-flight count. + /// + public override void JobsInFlightRemove(int tasksCount) + { + base.JobsInFlightRemove(tasksCount); + Metrics.RecordGauge("symbol_collector.jobs_in_flight", JobsInFlightCount); + } + + /// + /// Adds jobs to the in-flight count. + /// + public override void JobsInFlightAdd(int tasksCount) + { + base.JobsInFlightAdd(tasksCount); + Metrics.RecordGauge("symbol_collector.jobs_in_flight", JobsInFlightCount); + } + + /// + /// Records bytes uploaded, emitting as a distribution for percentile analysis. + /// + public override void UploadedBytesAdd(long bytes) + { + base.UploadedBytesAdd(bytes); + Metrics.RecordDistribution("symbol_collector.uploaded_bytes", bytes, "byte"); + } + + /// + /// Records an unauthorized access error. + /// + public override void FileOrDirectoryUnauthorizedAccess() + { + base.FileOrDirectoryUnauthorizedAccess(); + Metrics.AddCounter("symbol_collector.access_errors", 1, + [new KeyValuePair("type", "unauthorized")]); + } + + /// + /// Records a directory not found error. + /// + public override void DirectoryDoesNotExist() + { + base.DirectoryDoesNotExist(); + Metrics.AddCounter("symbol_collector.access_errors", 1, + [new KeyValuePair("type", "directory_not_found")]); + } + + /// + /// Records a file not found error. + /// + public override void FileDoesNotExist() + { + base.FileDoesNotExist(); + Metrics.AddCounter("symbol_collector.access_errors", 1, + [new KeyValuePair("type", "file_not_found")]); + } +} diff --git a/src/SymbolCollector.Core/Startup.cs b/src/SymbolCollector.Core/Startup.cs index 4dd298b..75da8fe 100644 --- a/src/SymbolCollector.Core/Startup.cs +++ b/src/SymbolCollector.Core/Startup.cs @@ -48,9 +48,9 @@ private static void ConfigureServices(IServiceCollection services) { services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(); - services.AddSingleton(); services.AddOptions() .Configure((o, f) => f.Bind("SymbolClient", o)) diff --git a/src/SymbolCollector.Server/Program.cs b/src/SymbolCollector.Server/Program.cs index f137a23..de5df02 100644 --- a/src/SymbolCollector.Server/Program.cs +++ b/src/SymbolCollector.Server/Program.cs @@ -75,6 +75,7 @@ private static IHostBuilder CreateHostBuilder(string[] args) => o.MinimumBreadcrumbLevel = LogLevel.Debug; o.CaptureFailedRequests = true; o.EnableLogs = true; + o.Experimental.EnableMetrics = true; // https://github.com/getsentry/symbol-collector/issues/205 // o.CaptureBlockingCalls = true; diff --git a/src/SymbolCollector.Server/Startup.cs b/src/SymbolCollector.Server/Startup.cs index b8bab63..5ac86db 100644 --- a/src/SymbolCollector.Server/Startup.cs +++ b/src/SymbolCollector.Server/Startup.cs @@ -24,7 +24,8 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService()); services.AddSingleton(); services.AddSingleton(); services.AddSingleton();