feat(core): add alert notification sinks

This commit is contained in:
dualfroz committed 2026-08-25 23:10:39 +02:00
1 parent 74ac699796
commit 69a71b82f8
6 files changed
+126

No files matched your search

@@ -0,0 +1,26 @@
namespace Vantage.Core.Alerts;
public sealed class BufferedDesktopNotifier : IDesktopNotifier
{
public readonly record struct Toast(string Title, string Body, AlertSeverity Severity, DateTimeOffset At);
private readonly object gate = new();
private readonly List<Toast> pending = new();
public Task ShowAsync(string title, string body, AlertSeverity severity, CancellationToken cancellationToken = default)
{
lock (gate)
pending.Add(new Toast(title, body, severity, DateTimeOffset.UtcNow));
return Task.CompletedTask;
}
public IReadOnlyList<Toast> DrainPending()
{
lock (gate)
{
var drained = pending.ToArray();
pending.Clear();
return drained;
}
}
}
@@ -0,0 +1,19 @@
namespace Vantage.Core.Alerts;
public sealed class DesktopAlertSink : IAlertSink
{
private readonly IDesktopNotifier notifier;
public DesktopAlertSink(IDesktopNotifier notifier)
{
this.notifier = notifier;
}
public Task NotifyAsync(AlertNotification notification, CancellationToken cancellationToken = default)
{
var title = notification.Kind == AlertTransitionKind.Fired
? $"Alert: {notification.RuleName}"
: $"Resolved: {notification.RuleName}";
return notifier.ShowAsync(title, notification.Summary(), notification.Severity, cancellationToken);
}
}
+6
View File
@@ -0,0 +1,6 @@
namespace Vantage.Core.Alerts;
public interface IAlertSink
{
Task NotifyAsync(AlertNotification notification, CancellationToken cancellationToken = default);
}
@@ -0,0 +1,6 @@
namespace Vantage.Core.Alerts;
public interface IDesktopNotifier
{
Task ShowAsync(string title, string body, AlertSeverity severity, CancellationToken cancellationToken = default);
}
+17
View File
@@ -0,0 +1,17 @@
namespace Vantage.Core.Alerts;
public sealed class LogAlertSink : IAlertSink
{
private readonly IAlertLog log;
public LogAlertSink(IAlertLog log)
{
this.log = log;
}
public Task NotifyAsync(AlertNotification notification, CancellationToken cancellationToken = default)
{
log.Append(new AlertLogEntry(notification.Timestamp, notification.Severity, notification.Summary()));
return Task.CompletedTask;
}
}
@@ -0,0 +1,52 @@
using System.Net.Http.Json;
namespace Vantage.Core.Alerts;
public sealed class WebhookAlertSink : IAlertSink
{
private readonly HttpClient httpClient;
private readonly Uri endpoint;
private readonly TimeSpan timeout;
public WebhookAlertSink(HttpClient httpClient, Uri endpoint, TimeSpan? timeout = null)
{
this.httpClient = httpClient;
this.endpoint = endpoint;
this.timeout = timeout ?? TimeSpan.FromSeconds(10);
}
public async Task NotifyAsync(AlertNotification notification, CancellationToken cancellationToken = default)
{
var payload = new WebhookPayload(
Status: notification.Kind == AlertTransitionKind.Fired ? "firing" : "resolved",
Severity: notification.Severity.ToString().ToLowerInvariant(),
Rule: notification.RuleName,
Host: notification.HostId,
Metric: notification.Metric,
Value: notification.Value,
Threshold: notification.Threshold,
Comparison: notification.ComparisonSymbol,
Summary: notification.Summary(),
Timestamp: notification.Timestamp.ToUnixTimeMilliseconds());
using var timeoutCts = new CancellationTokenSource(timeout);
using var linked = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
using var response = await httpClient
.PostAsJsonAsync(endpoint, payload, linked.Token)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
}
private sealed record WebhookPayload(
string Status,
string Severity,
string Rule,
string Host,
string Metric,
double Value,
double Threshold,
string Comparison,
string Summary,
long Timestamp);
}