diff --git a/Api.SeaHavenIndustries/Helper/VendorDocumentScanWorker.cs b/Api.SeaHavenIndustries/Helper/VendorDocumentScanWorker.cs index c77e5b6..c236ab5 100644 --- a/Api.SeaHavenIndustries/Helper/VendorDocumentScanWorker.cs +++ b/Api.SeaHavenIndustries/Helper/VendorDocumentScanWorker.cs @@ -1,5 +1,6 @@ using Data.SeaHavenIndustries; using Microsoft.EntityFrameworkCore; +using Sentry; namespace Api.SeaHavenIndustries.Helper { @@ -8,15 +9,18 @@ namespace Api.SeaHavenIndustries.Helper private readonly IServiceScopeFactory _scopeFactory; private readonly IWebHostEnvironment _environment; private readonly ILogger _logger; + private readonly IHub _sentryHub; public VendorDocumentScanWorker( IServiceScopeFactory scopeFactory, IWebHostEnvironment environment, - ILogger logger) + ILogger logger, + IHub sentryHub) { _scopeFactory = scopeFactory; _environment = environment; _logger = logger; + _sentryHub = sentryHub; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -30,36 +34,54 @@ namespace Api.SeaHavenIndustries.Helper private async Task ScanPendingAsync(CancellationToken cancellationToken) { - using var scope = _scopeFactory.CreateScope(); - var db = scope.ServiceProvider.GetRequiredService(); - var scanner = scope.ServiceProvider.GetRequiredService(); - var pending = await db.VendorCompletionDocuments - .Where(document => document.ScanStatus == "Pending") - .OrderBy(document => document.CreatedDate) - .Take(20) - .ToListAsync(cancellationToken); - - foreach (var document in pending) + using var sentryScope = _sentryHub.PushScope(); + var transaction = _sentryHub.StartTransaction("vendor-documents.scan-pending", "task"); + _sentryHub.ConfigureScope(scope => scope.Transaction = transaction); + try { - var path = VendorDocumentStorage.ResolvePath(_environment.ContentRootPath, document); - var result = await scanner.ScanAsync(path, cancellationToken); - if (result == DocumentScanResult.Unavailable) + using var scope = _scopeFactory.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var scanner = scope.ServiceProvider.GetRequiredService(); + var pending = await db.VendorCompletionDocuments + .Where(document => document.ScanStatus == "Pending") + .OrderBy(document => document.CreatedDate) + .Take(20) + .ToListAsync(cancellationToken); + + foreach (var document in pending) { - _logger.LogWarning("Vendor document scan service unavailable; document {DocumentId} remains quarantined", document.Id); - continue; + var path = VendorDocumentStorage.ResolvePath(_environment.ContentRootPath, document); + var result = await scanner.ScanAsync(path, cancellationToken); + if (result == DocumentScanResult.Unavailable) + { + _logger.LogWarning("Vendor document scan service unavailable; document {DocumentId} remains quarantined", document.Id); + continue; + } + + document.ScanStatus = result == DocumentScanResult.Passed ? "Passed" : "Rejected"; + document.ReviewStatus = result == DocumentScanResult.Passed ? "Processing" : "Rejected"; + document.RejectionReason = result == DocumentScanResult.Infected + ? "The upload failed malware scanning." + : document.RejectionReason; + document.ScannedAt = DateTime.UtcNow; + document.LastModificationTime = DateTime.UtcNow; + await db.SaveChangesAsync(cancellationToken); + + if (result == DocumentScanResult.Infected && File.Exists(path)) + File.Delete(path); } - document.ScanStatus = result == DocumentScanResult.Passed ? "Passed" : "Rejected"; - document.ReviewStatus = result == DocumentScanResult.Passed ? "Processing" : "Rejected"; - document.RejectionReason = result == DocumentScanResult.Infected - ? "The upload failed malware scanning." - : document.RejectionReason; - document.ScannedAt = DateTime.UtcNow; - document.LastModificationTime = DateTime.UtcNow; - await db.SaveChangesAsync(cancellationToken); - - if (result == DocumentScanResult.Infected && File.Exists(path)) - File.Delete(path); + transaction.Finish(SpanStatus.Ok); + } + catch (OperationCanceledException) + { + transaction.Finish(SpanStatus.Cancelled); + throw; + } + catch (Exception ex) + { + transaction.Finish(ex, SpanStatus.InternalError); + throw; } } } diff --git a/Api.SeaHavenIndustries/HostedServices/PastDueCacheHostedService.cs b/Api.SeaHavenIndustries/HostedServices/PastDueCacheHostedService.cs index 6daf1f7..48e2de7 100644 --- a/Api.SeaHavenIndustries/HostedServices/PastDueCacheHostedService.cs +++ b/Api.SeaHavenIndustries/HostedServices/PastDueCacheHostedService.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Options; using SeaHaven.Services.Implementation; using SeaHaven.Services.Interfaces; using Api.SeaHavenIndustries.Options; +using Sentry; namespace Api.SeaHavenIndustries.HostedServices { @@ -11,18 +12,21 @@ namespace Api.SeaHavenIndustries.HostedServices private readonly ILogger _logger; private readonly PastDueCacheJobOptions _options; private readonly WorkOrderJobRunState _runState; + private readonly IHub _sentryHub; private DateOnly? _lastRunDate; public PastDueCacheHostedService( IServiceScopeFactory scopeFactory, IOptions options, WorkOrderJobRunState runState, - ILogger logger) + ILogger logger, + IHub sentryHub) { _scopeFactory = scopeFactory; _logger = logger; _options = options.Value.PastDueCache; _runState = runState; + _sentryHub = sentryHub; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -45,6 +49,9 @@ namespace Api.SeaHavenIndustries.HostedServices private async Task RunJobAsync(CancellationToken stoppingToken) { + using var sentryScope = _sentryHub.PushScope(); + var transaction = _sentryHub.StartTransaction("workorders.past-due-cache-job", "task"); + _sentryHub.ConfigureScope(scope => scope.Transaction = transaction); try { using var scope = _scopeFactory.CreateScope(); @@ -53,12 +60,19 @@ namespace Api.SeaHavenIndustries.HostedServices _runState.LastPastDueCacheRunUtc = DateTime.UtcNow; _runState.LastPastDueCacheError = null; + transaction.Finish(SpanStatus.Ok); return true; } + catch (OperationCanceledException) + { + transaction.Finish(SpanStatus.Cancelled); + throw; + } catch (Exception ex) when (ex is not OperationCanceledException) { _runState.LastPastDueCacheError = ex.Message; _logger.LogError(ex, "PastDue cache hosted job failed."); + transaction.Finish(ex, SpanStatus.InternalError); return false; } } diff --git a/Api.SeaHavenIndustries/HostedServices/UpliftLifecycleHostedService.cs b/Api.SeaHavenIndustries/HostedServices/UpliftLifecycleHostedService.cs index 23258f1..d4de208 100644 --- a/Api.SeaHavenIndustries/HostedServices/UpliftLifecycleHostedService.cs +++ b/Api.SeaHavenIndustries/HostedServices/UpliftLifecycleHostedService.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Options; using SeaHaven.Services.Configuration; using SeaHaven.Services.Interfaces; +using Sentry; namespace Api.SeaHavenIndustries.HostedServices { @@ -9,32 +10,43 @@ namespace Api.SeaHavenIndustries.HostedServices private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger _logger; private readonly ApprovalsOptions _options; + private readonly IHub _sentryHub; public UpliftLifecycleHostedService( IServiceScopeFactory scopeFactory, IOptions options, - ILogger logger) + ILogger logger, + IHub sentryHub) { _scopeFactory = scopeFactory; _logger = logger; _options = options.Value; + _sentryHub = sentryHub; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { - try { - await SweepAsync(stoppingToken); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Uplift lifecycle sweep failed; will retry on next interval"); + using var sentryScope = _sentryHub.PushScope(); + var transaction = _sentryHub.StartTransaction("uplifts.lifecycle-sweep", "task"); + _sentryHub.ConfigureScope(scope => scope.Transaction = transaction); + try + { + await SweepAsync(stoppingToken); + transaction.Finish(SpanStatus.Ok); + } + catch (OperationCanceledException) + { + transaction.Finish(SpanStatus.Cancelled); + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Uplift lifecycle sweep failed; will retry on next interval"); + transaction.Finish(ex, SpanStatus.InternalError); + } } await Task.Delay(_options.EffectiveSweepInterval, stoppingToken); diff --git a/Api.SeaHavenIndustries/HostedServices/WorkOrderReconciliationHostedService.cs b/Api.SeaHavenIndustries/HostedServices/WorkOrderReconciliationHostedService.cs index 616f7aa..0f74200 100644 --- a/Api.SeaHavenIndustries/HostedServices/WorkOrderReconciliationHostedService.cs +++ b/Api.SeaHavenIndustries/HostedServices/WorkOrderReconciliationHostedService.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Options; using SeaHaven.Services.Configuration; using SeaHaven.Services.Interfaces; +using Sentry; namespace Api.SeaHavenIndustries.HostedServices { @@ -10,17 +11,20 @@ namespace Api.SeaHavenIndustries.HostedServices private readonly IOptionsMonitor _options; private readonly TimeProvider _timeProvider; private readonly ILogger _logger; + private readonly IHub _sentryHub; public WorkOrderReconciliationHostedService( IServiceScopeFactory scopeFactory, IOptionsMonitor options, TimeProvider timeProvider, - ILogger logger) + ILogger logger, + IHub sentryHub) { _scopeFactory = scopeFactory; _options = options; _timeProvider = timeProvider; _logger = logger; + _sentryHub = sentryHub; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -56,29 +60,50 @@ namespace Api.SeaHavenIndustries.HostedServices private async Task TriggerAsync(string reason, CancellationToken cancellationToken) { + using var sentryScope = _sentryHub.PushScope(); + var transaction = _sentryHub.StartTransaction("workorders.reconciliation-trigger", "task"); + transaction.Description = reason; + _sentryHub.ConfigureScope(scope => scope.Transaction = transaction); try { await using var scope = _scopeFactory.CreateAsyncScope(); var runner = scope.ServiceProvider.GetRequiredService(); await runner.TriggerAsync(reason, cancellationToken); + transaction.Finish(SpanStatus.Ok); + } + catch (OperationCanceledException) + { + transaction.Finish(SpanStatus.Cancelled); + throw; } catch (Exception ex) when (ex is not OperationCanceledException) { _logger.LogError(ex, "Could not queue procurement reconciliation."); + transaction.Finish(ex, SpanStatus.InternalError); } } private async Task RunPendingAsync(CancellationToken cancellationToken) { + using var sentryScope = _sentryHub.PushScope(); + var transaction = _sentryHub.StartTransaction("workorders.reconciliation-run-pending", "task"); + _sentryHub.ConfigureScope(scope => scope.Transaction = transaction); try { await using var scope = _scopeFactory.CreateAsyncScope(); var runner = scope.ServiceProvider.GetRequiredService(); await runner.RunPendingAsync(cancellationToken); + transaction.Finish(SpanStatus.Ok); + } + catch (OperationCanceledException) + { + transaction.Finish(SpanStatus.Cancelled); + throw; } catch (Exception ex) when (ex is not OperationCanceledException) { _logger.LogError(ex, "Could not execute procurement reconciliation."); + transaction.Finish(ex, SpanStatus.InternalError); } } } diff --git a/Api.SeaHavenIndustries/HostedServices/WorkOrderWeekRolledHostedService.cs b/Api.SeaHavenIndustries/HostedServices/WorkOrderWeekRolledHostedService.cs index dc5a309..0ac73ae 100644 --- a/Api.SeaHavenIndustries/HostedServices/WorkOrderWeekRolledHostedService.cs +++ b/Api.SeaHavenIndustries/HostedServices/WorkOrderWeekRolledHostedService.cs @@ -3,6 +3,7 @@ using SeaHaven.Services.Helpers; using SeaHaven.Services.Implementation; using SeaHaven.Services.Interfaces; using Api.SeaHavenIndustries.Options; +using Sentry; namespace Api.SeaHavenIndustries.HostedServices { @@ -12,18 +13,21 @@ namespace Api.SeaHavenIndustries.HostedServices private readonly ILogger _logger; private readonly WeekRolledJobOptions _options; private readonly WorkOrderJobRunState _runState; + private readonly IHub _sentryHub; private DateOnly? _lastRunDate; public WorkOrderWeekRolledHostedService( IServiceScopeFactory scopeFactory, IOptions options, WorkOrderJobRunState runState, - ILogger logger) + ILogger logger, + IHub sentryHub) { _scopeFactory = scopeFactory; _logger = logger; _options = options.Value.WeekRolled; _runState = runState; + _sentryHub = sentryHub; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -46,6 +50,9 @@ namespace Api.SeaHavenIndustries.HostedServices private async Task RunJobAsync(CancellationToken stoppingToken) { + using var sentryScope = _sentryHub.PushScope(); + var transaction = _sentryHub.StartTransaction("workorders.week-rolled-job", "task"); + _sentryHub.ConfigureScope(scope => scope.Transaction = transaction); try { var sourceWeekStart = WorkOrderOperationalWeek.GetPreviousOperationalWeekStart(DateTime.UtcNow); @@ -56,12 +63,19 @@ namespace Api.SeaHavenIndustries.HostedServices _runState.LastWeekRolledRunUtc = DateTime.UtcNow; _runState.LastWeekRolledError = null; + transaction.Finish(SpanStatus.Ok); return true; } + catch (OperationCanceledException) + { + transaction.Finish(SpanStatus.Cancelled); + throw; + } catch (Exception ex) when (ex is not OperationCanceledException) { _runState.LastWeekRolledError = ex.Message; _logger.LogError(ex, "WeekRolled hosted job failed."); + transaction.Finish(ex, SpanStatus.InternalError); return false; } }