fix: trace background jobs in Sentry

This commit is contained in:
Alexandre Brandizzi 2026-09-03 16:03:25 -03:00
parent 42f66fbc69
commit ae68b9449f
5 changed files with 128 additions and 41 deletions

View file

@ -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<VendorDocumentScanWorker> _logger;
private readonly IHub _sentryHub;
public VendorDocumentScanWorker(
IServiceScopeFactory scopeFactory,
IWebHostEnvironment environment,
ILogger<VendorDocumentScanWorker> logger)
ILogger<VendorDocumentScanWorker> 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<ApplicationDbContext>();
var scanner = scope.ServiceProvider.GetRequiredService<IDocumentScanner>();
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<ApplicationDbContext>();
var scanner = scope.ServiceProvider.GetRequiredService<IDocumentScanner>();
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;
}
}
}

View file

@ -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<PastDueCacheHostedService> _logger;
private readonly PastDueCacheJobOptions _options;
private readonly WorkOrderJobRunState _runState;
private readonly IHub _sentryHub;
private DateOnly? _lastRunDate;
public PastDueCacheHostedService(
IServiceScopeFactory scopeFactory,
IOptions<WorkOrderJobsOptions> options,
WorkOrderJobRunState runState,
ILogger<PastDueCacheHostedService> logger)
ILogger<PastDueCacheHostedService> 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<bool> 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;
}
}

View file

@ -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<UpliftLifecycleHostedService> _logger;
private readonly ApprovalsOptions _options;
private readonly IHub _sentryHub;
public UpliftLifecycleHostedService(
IServiceScopeFactory scopeFactory,
IOptions<ApprovalsOptions> options,
ILogger<UpliftLifecycleHostedService> logger)
ILogger<UpliftLifecycleHostedService> 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);

View file

@ -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<WorkOrderReconciliationOptions> _options;
private readonly TimeProvider _timeProvider;
private readonly ILogger<WorkOrderReconciliationHostedService> _logger;
private readonly IHub _sentryHub;
public WorkOrderReconciliationHostedService(
IServiceScopeFactory scopeFactory,
IOptionsMonitor<WorkOrderReconciliationOptions> options,
TimeProvider timeProvider,
ILogger<WorkOrderReconciliationHostedService> logger)
ILogger<WorkOrderReconciliationHostedService> 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<IWorkOrderReconciliationRunner>();
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<IWorkOrderReconciliationRunner>();
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);
}
}
}

View file

@ -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<WorkOrderWeekRolledHostedService> _logger;
private readonly WeekRolledJobOptions _options;
private readonly WorkOrderJobRunState _runState;
private readonly IHub _sentryHub;
private DateOnly? _lastRunDate;
public WorkOrderWeekRolledHostedService(
IServiceScopeFactory scopeFactory,
IOptions<WorkOrderJobsOptions> options,
WorkOrderJobRunState runState,
ILogger<WorkOrderWeekRolledHostedService> logger)
ILogger<WorkOrderWeekRolledHostedService> 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<bool> 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;
}
}