feat(core): add alert rule model and evaluator

This commit is contained in:
dualfroz committed 2026-08-25 18:47:36 +02:00
1 parent 1236103658
commit da6f8d2d51
5 files changed
+211

No files matched your search

+49
View File
@@ -0,0 +1,49 @@
namespace Vantage.Core.Alerts;
public enum AlertComparison
{
GreaterThan,
GreaterOrEqual,
LessThan,
LessOrEqual
}
public enum AlertSeverity
{
Info,
Warning,
Critical
}
public sealed class AlertRule
{
public string Id { get; set; } = Guid.NewGuid().ToString("N");
public string Name { get; set; } = "";
public string? HostId { get; set; }
public string Metric { get; set; } = "";
public AlertComparison Comparison { get; set; } = AlertComparison.GreaterThan;
public double Threshold { get; set; }
public TimeSpan For { get; set; } = TimeSpan.Zero;
public AlertSeverity Severity { get; set; } = AlertSeverity.Warning;
public bool Enabled { get; set; } = true;
public bool AppliesTo(string hostId) => HostId is null || HostId == hostId;
public bool IsBreaching(double value) => Comparison switch
{
AlertComparison.GreaterThan => value > Threshold,
AlertComparison.GreaterOrEqual => value >= Threshold,
AlertComparison.LessThan => value < Threshold,
AlertComparison.LessOrEqual => value <= Threshold,
_ => false
};
public string ComparisonSymbol => Comparison switch
{
AlertComparison.GreaterThan => ">",
AlertComparison.GreaterOrEqual => ">=",
AlertComparison.LessThan => "<",
AlertComparison.LessOrEqual => "<=",
_ => "?"
};
}
@@ -0,0 +1,71 @@
namespace Vantage.Core.Alerts;
public sealed class AlertRuleEvaluator
{
private readonly AlertRule rule;
private readonly string hostId;
private DateTimeOffset? breachingSince;
public AlertRuleEvaluator(AlertRule rule, string hostId)
{
this.rule = rule;
this.hostId = hostId;
}
public AlertState State { get; private set; } = AlertState.Ok;
public DateTimeOffset Since { get; private set; }
public double LastValue { get; private set; }
public AlertNotification? Update(double value, DateTimeOffset now)
{
LastValue = value;
if (rule.IsBreaching(value))
return OnBreach(value, now);
return OnClear(value, now);
}
private AlertNotification? OnBreach(double value, DateTimeOffset now)
{
breachingSince ??= now;
if (State == AlertState.Firing)
return null;
var breachedFor = now - breachingSince.Value;
if (breachedFor >= rule.For)
{
State = AlertState.Firing;
Since = now;
return Notification(AlertTransitionKind.Fired, value, now);
}
if (State != AlertState.Pending)
{
State = AlertState.Pending;
Since = breachingSince.Value;
}
return null;
}
private AlertNotification? OnClear(double value, DateTimeOffset now)
{
breachingSince = null;
var wasFiring = State == AlertState.Firing;
var changed = State != AlertState.Ok;
State = AlertState.Ok;
if (changed)
Since = now;
return wasFiring ? Notification(AlertTransitionKind.Resolved, value, now) : null;
}
public AlertStatus Snapshot() =>
new(rule.Id, rule.Name, hostId, rule.Metric, rule.Severity, State, LastValue, rule.Threshold, rule.ComparisonSymbol, Since);
private AlertNotification Notification(AlertTransitionKind kind, double value, DateTimeOffset now) =>
new(rule.Id, rule.Name, hostId, rule.Metric, rule.Severity, kind, value, rule.Threshold, rule.ComparisonSymbol, rule.For, now);
}
+48
View File
@@ -0,0 +1,48 @@
namespace Vantage.Core.Alerts;
public enum AlertState
{
Ok,
Pending,
Firing
}
public enum AlertTransitionKind
{
Fired,
Resolved
}
public sealed record AlertNotification(
string RuleId,
string RuleName,
string HostId,
string Metric,
AlertSeverity Severity,
AlertTransitionKind Kind,
double Value,
double Threshold,
string ComparisonSymbol,
TimeSpan For,
DateTimeOffset Timestamp)
{
public string Summary()
{
var scope = $"{Metric} {ComparisonSymbol} {Threshold:0.##}";
return Kind == AlertTransitionKind.Fired
? $"FIRING [{Severity}] {RuleName} on {HostId}: {scope} (is {Value:0.##})"
: $"RESOLVED [{Severity}] {RuleName} on {HostId}: {scope} (is {Value:0.##})";
}
}
public sealed record AlertStatus(
string RuleId,
string RuleName,
string HostId,
string Metric,
AlertSeverity Severity,
AlertState State,
double LastValue,
double Threshold,
string ComparisonSymbol,
DateTimeOffset Since);
+10
View File
@@ -0,0 +1,10 @@
namespace Vantage.Core.Alerts;
public sealed record AlertLogEntry(DateTimeOffset Timestamp, AlertSeverity Severity, string Line);
public interface IAlertLog
{
void Append(AlertLogEntry entry);
IReadOnlyList<AlertLogEntry> Recent(int max);
}
@@ -0,0 +1,33 @@
namespace Vantage.Core.Alerts;
public sealed class InMemoryAlertLog : IAlertLog
{
private readonly object gate = new();
private readonly LinkedList<AlertLogEntry> entries = new();
private readonly int capacity;
public InMemoryAlertLog(int capacity = 500)
{
if (capacity < 1)
throw new ArgumentOutOfRangeException(nameof(capacity));
this.capacity = capacity;
}
public void Append(AlertLogEntry entry)
{
lock (gate)
{
entries.AddLast(entry);
while (entries.Count > capacity)
entries.RemoveFirst();
}
}
public IReadOnlyList<AlertLogEntry> Recent(int max)
{
lock (gate)
{
return entries.Reverse().Take(max).ToArray();
}
}
}