54 lines
1.9 KiB
C#
54 lines
1.9 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
|
|
public sealed class AlertsUnacknowledgedCollector : BackgroundService
|
|
{
|
|
// 5 minutes matches the DLQ TTL — an alert that survived escalation is still open.
|
|
private static readonly TimeSpan UnacknowledgedThreshold = TimeSpan.FromMinutes(5);
|
|
|
|
private readonly IServiceScopeFactory _scopes;
|
|
private readonly ClinicalMetrics _metrics;
|
|
private readonly ILogger<AlertsUnacknowledgedCollector> _logger;
|
|
|
|
public AlertsUnacknowledgedCollector(
|
|
IServiceScopeFactory scopes,
|
|
ClinicalMetrics metrics,
|
|
ILogger<AlertsUnacknowledgedCollector> logger)
|
|
{
|
|
_scopes = scopes;
|
|
_metrics = metrics;
|
|
_logger = logger;
|
|
}
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken ct)
|
|
{
|
|
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
|
|
while (await timer.WaitForNextTickAsync(ct))
|
|
await CollectAsync(ct);
|
|
}
|
|
|
|
private async Task CollectAsync(CancellationToken ct)
|
|
{
|
|
try
|
|
{
|
|
await using var scope = _scopes.CreateAsyncScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
var cutoff = DateTimeOffset.UtcNow - UnacknowledgedThreshold;
|
|
|
|
var count = await db.ClinicalAlerts
|
|
.CountAsync(a => a.Severity == AlertSeverity.Critical
|
|
&& a.Status == AlertStatus.Open
|
|
&& a.TriggeredAt < cutoff, ct);
|
|
|
|
_metrics.AlertsUnacknowledgedGauge.Set(count);
|
|
|
|
if (count > 0)
|
|
_logger.LogWarning(
|
|
"[PATIENT-SAFETY] alerts_unacknowledged_gauge={Count} " +
|
|
"(CRITICAL alerts open > 5 min)", count);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "AlertsUnacknowledgedCollector failed");
|
|
}
|
|
}
|
|
} |