feature: Trend Detection & Alert Suppression Windows
This commit is contained in:
@@ -1,11 +1,17 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
public class AlertService : IAlertService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
public AlertService(AppDbContext db) => _db = db;
|
||||
public AlertService(AppDbContext db, IServiceProvider services)
|
||||
{
|
||||
_db = db;
|
||||
_services = services;
|
||||
}
|
||||
|
||||
public async Task<PagedResult<ClinicalAlert>> ListByEncounterAsync(
|
||||
Guid encounterId, AlertStatus? status, int page, int pageSize)
|
||||
@@ -100,10 +106,42 @@ public class AlertService : IAlertService
|
||||
CreatedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
|
||||
if (alert.AlertType.IsSuppressible())
|
||||
{
|
||||
var suppression = _services.GetRequiredService<IAlertSuppressionService>();
|
||||
var options = _services.GetRequiredService<IOptions<SuppressionOptions>>().Value;
|
||||
var windowMinutes = await ResolveSuppressionWindowMinutesAsync(
|
||||
alert.AlertType, options.DefaultWindowMinutes);
|
||||
|
||||
await suppression.SetSuppressionAsync(
|
||||
alert.EncounterId,
|
||||
alert.AlertType,
|
||||
TimeSpan.FromMinutes(windowMinutes));
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return alert;
|
||||
}
|
||||
|
||||
private async Task<int> ResolveSuppressionWindowMinutesAsync(
|
||||
AlertType alertType, int defaultWindowMinutes)
|
||||
{
|
||||
if (alertType == AlertType.News2Warning)
|
||||
return defaultWindowMinutes;
|
||||
|
||||
var observationCode = alertType.ObservationCodeForWarning();
|
||||
if (observationCode is null)
|
||||
return defaultWindowMinutes;
|
||||
|
||||
var overrideMinutes = await _db.AlertThresholds
|
||||
.AsNoTracking()
|
||||
.Where(t => t.ObservationCode == observationCode)
|
||||
.Select(t => t.SuppressionWindowMinutes)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
return overrideMinutes ?? defaultWindowMinutes;
|
||||
}
|
||||
|
||||
public async Task<ClinicalAlert> ResolveAsync(Guid id)
|
||||
{
|
||||
var alert = await _db.ClinicalAlerts.FindAsync(id);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using StackExchange.Redis;
|
||||
|
||||
public class AlertSuppressionService : IAlertSuppressionService
|
||||
{
|
||||
private readonly IConnectionMultiplexer _redis;
|
||||
private readonly ClinicalMetrics _metrics;
|
||||
|
||||
public AlertSuppressionService(IConnectionMultiplexer redis, ClinicalMetrics metrics)
|
||||
{
|
||||
_redis = redis;
|
||||
_metrics = metrics;
|
||||
}
|
||||
|
||||
public static string SuppressionKey(Guid encounterId, AlertType alertType) =>
|
||||
$"suppress:{encounterId}:{alertType.ToDbString()}";
|
||||
|
||||
public async Task SetSuppressionAsync(
|
||||
Guid encounterId, AlertType alertType, TimeSpan ttl, CancellationToken ct = default)
|
||||
{
|
||||
var cache = _redis.GetDatabase();
|
||||
await cache.StringSetAsync(SuppressionKey(encounterId, alertType), "1", ttl);
|
||||
_metrics.AlertSuppressionsTotal.WithLabels(alertType.ToDbString()).Inc();
|
||||
}
|
||||
|
||||
public async Task<bool> IsSuppressedAsync(
|
||||
Guid encounterId, AlertType alertType, CancellationToken ct = default)
|
||||
{
|
||||
var cache = _redis.GetDatabase();
|
||||
return await cache.KeyExistsAsync(SuppressionKey(encounterId, alertType));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
public interface IAlertSuppressionService
|
||||
{
|
||||
Task SetSuppressionAsync(Guid encounterId, AlertType alertType, TimeSpan ttl, CancellationToken ct = default);
|
||||
Task<bool> IsSuppressedAsync(Guid encounterId, AlertType alertType, CancellationToken ct = default);
|
||||
}
|
||||
@@ -35,6 +35,15 @@ public class WarningEvaluator
|
||||
// critical alerts are created synchronously by the ingest path.
|
||||
if (IsCriticalBreach(value, threshold)) return false;
|
||||
|
||||
var alertType = AlertTypeExtensions.WarningFor(observationCode);
|
||||
var suppression = _services.GetRequiredService<IAlertSuppressionService>();
|
||||
if (await suppression.IsSuppressedAsync(encounterId, alertType, ct))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Warning alert suppressed for encounter {Id} type {Type}", encounterId, alertType);
|
||||
return false;
|
||||
}
|
||||
|
||||
return await TryCreateWarningAlertAsync(
|
||||
observationId, encounterId, patientId, observationCode, value, threshold, ct);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user