Finish: Phase 33 — Alert Quality Analytics
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
public class BadRequestException : DomainException
|
||||
{
|
||||
public BadRequestException(string message, string errorCode = "BAD_REQUEST")
|
||||
: base(message, errorCode) { }
|
||||
}
|
||||
@@ -21,6 +21,12 @@ public class ExceptionHandlerMiddleware
|
||||
await WriteAsync(context, StatusCodes.Status404NotFound,
|
||||
ApiResponse<object>.Fail(StatusCodes.Status404NotFound, ex.Message, ex.ErrorCode));
|
||||
}
|
||||
catch (BadRequestException ex)
|
||||
{
|
||||
_logger.LogWarning("{Message}", ex.Message);
|
||||
await WriteAsync(context, StatusCodes.Status400BadRequest,
|
||||
ApiResponse<object>.Fail(StatusCodes.Status400BadRequest, ex.Message, ex.ErrorCode));
|
||||
}
|
||||
catch (ValidationException ex)
|
||||
{
|
||||
_logger.LogWarning("{Message}", ex.Message);
|
||||
|
||||
@@ -86,17 +86,7 @@ public sealed class RabbitMqTopologyProvisioner : IHostedService
|
||||
// Messages land here after NACK from the paging worker.
|
||||
// After x-message-ttl expires, re-routes to clinical.notifications.exchange
|
||||
// with routing key alerts.escalation → reaches alerts.escalation.queue.
|
||||
channel.QueueDeclare(
|
||||
queue: "alerts.paging.dlq",
|
||||
durable: true,
|
||||
exclusive: false,
|
||||
autoDelete: false,
|
||||
arguments: new Dictionary<string, object>
|
||||
{
|
||||
["x-message-ttl"] = (int)_opts.PagingAckTimeoutMs,
|
||||
["x-dead-letter-exchange"] = Exchange,
|
||||
["x-dead-letter-routing-key"] = EscalKey,
|
||||
});
|
||||
DeclarePagingDlq(connection, channel);
|
||||
// DLQ is reached via the default exchange — no binding to the direct exchange needed.
|
||||
|
||||
// --- alerts.escalation.queue ---
|
||||
@@ -153,6 +143,34 @@ public sealed class RabbitMqTopologyProvisioner : IHostedService
|
||||
|
||||
public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
|
||||
|
||||
private void DeclarePagingDlq(IConnection connection, IModel channel)
|
||||
{
|
||||
const string dlq = "alerts.paging.dlq";
|
||||
var args = new Dictionary<string, object>
|
||||
{
|
||||
["x-message-ttl"] = _opts.PagingAckTimeoutMs,
|
||||
["x-dead-letter-exchange"] = Exchange,
|
||||
["x-dead-letter-routing-key"] = EscalKey,
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
channel.QueueDeclare(dlq, durable: true, exclusive: false, autoDelete: false, arguments: args);
|
||||
}
|
||||
catch (OperationInterruptedException ex)
|
||||
{
|
||||
// Another process (e.g. local dev API) may have created the DLQ with a different
|
||||
// x-message-ttl. Delete and recreate so PagingAckTimeoutMs drives escalation timing.
|
||||
_logger.LogWarning(ex,
|
||||
"Paging DLQ arguments mismatch — deleting and recreating with PagingDlqTtlMs={Ttl}",
|
||||
_opts.PagingAckTimeoutMs);
|
||||
|
||||
using var cleanup = connection.CreateModel();
|
||||
cleanup.QueueDelete(dlq, ifUnused: false, ifEmpty: false);
|
||||
cleanup.QueueDeclare(dlq, durable: true, exclusive: false, autoDelete: false, arguments: args);
|
||||
}
|
||||
}
|
||||
|
||||
public IConnectionFactory BuildFactory() => new ConnectionFactory
|
||||
{
|
||||
HostName = _opts.Host,
|
||||
|
||||
@@ -176,7 +176,7 @@ public class AlertService : IAlertService
|
||||
throw new NotFoundException("Alert not found.", "ALERT_NOT_FOUND");
|
||||
|
||||
if (alert.Status == AlertStatus.Open)
|
||||
throw new ValidationException(
|
||||
throw new BadRequestException(
|
||||
"Feedback can only be submitted on acknowledged or resolved alerts.",
|
||||
"ALERT_NOT_REVIEWABLE");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user