feat(core): add alert rule model and evaluator
This commit is contained in:
1 parent
1236103658
commit
da6f8d2d51
5 files changed
+211
No files matched your search
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user