Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ SAMA is a comprehensive service availability monitoring solution that helps you:
### Core Monitoring
- **Multiple Check Types**: HTTP/HTTPS, TCP, ICMP Ping, DNS, TLS certificates, Custom Scripts
- **Flexible Scheduling**: Per-check intervals from seconds to hours
- **Traffic Light Status**: Up (healthy), Warn (warning/degraded), Down (failed)
- **Traffic Light Status**: Up (healthy), Degraded (impaired), Down (failed)
- **Configurable Thresholds**: Require N consecutive failures before alerting

### Alerting
- **Reusable Channels**: Define notification channels once, use across multiple checks
- **Multiple Channels**: Email, Slack, Microsoft Teams, Discord, custom scripts, Azure Event Grid
- **Flexible Alerts**: Trigger on Warn/Down status with consecutive failure thresholds
- **Flexible Alerts**: Trigger on Degraded/Down status with consecutive failure thresholds
- **Recovery Notifications**: Optional notifications when services recover
- **Lifecycle Events**: Subscribe to check creation, updates, deletion, and status changes
- **External Integrations**: Send events to Azure Event Grid or custom scripts for workflow automation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ await _mockEventService.Received(1).TriggerLifecycleEventAsync(
ctx.PerformedBy == "testuser" &&
ctx.ConfigurationChanges != null &&
ctx.ConfigurationChanges.ContainsKey("Alert 'Original Alert' (renamed to 'Updated Alert'): Name") &&
ctx.ConfigurationChanges.ContainsKey("Alert 'Original Alert' (renamed to 'Updated Alert'): Trigger on Warn") &&
ctx.ConfigurationChanges.ContainsKey("Alert 'Original Alert' (renamed to 'Updated Alert'): Trigger on Degraded") &&
ctx.ConfigurationChanges.ContainsKey("Alert 'Original Alert' (renamed to 'Updated Alert'): Failure Threshold") &&
ctx.ConfigurationChanges.ContainsKey("Alert 'Original Alert' (renamed to 'Updated Alert'): Send Recovery Notification") &&
ctx.ConfigurationChanges.ContainsKey("Alert 'Original Alert' (renamed to 'Updated Alert'): Enabled") &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -741,24 +741,73 @@ public async Task GetWorkspaceIncidentTimelineAsyncShouldLimitToMaxChecks()
}

[TestMethod]
public async Task GetWorkspaceIncidentTimelineAsyncShouldOnlyIncludeEnabledChecks()
public async Task GetWorkspaceIncidentTimelineAsyncShouldIncludeDisabledCheckHistoricalData()
{
var enabledCheck = await CreateCheckAsync("Enabled Check", CheckTypes.Http, "60", true);
var disabledCheck = await CreateCheckAsync("Disabled Check", CheckTypes.Http, "60", false);

// The disabled check has results in two different increments.
// With hours=1, incrementMinutes=5. The cutoff is computed from the latest result,
// so the earlier result's increment is preserved while the latest one is excluded.
var earlyTime = DateTimeOffset.UtcNow.AddMinutes(-40);
var laterTime = DateTimeOffset.UtcNow.AddMinutes(-10);
var recentTime = DateTimeOffset.UtcNow.AddSeconds(-1);
await CreateCheckResultAsync(enabledCheck.Id, CheckStatuses.Up, recentTime);
await CreateCheckResultAsync(disabledCheck.Id, CheckStatuses.Down, earlyTime);
await CreateCheckResultAsync(disabledCheck.Id, CheckStatuses.Down, laterTime);

var result = await _service.GetWorkspaceIncidentTimelineAsync(_workspace.Id, 1);

Assert.IsNotNull(result);
Assert.IsNotEmpty(result.Increments);

// The early increment should contain the disabled check's historical data
var earlyIncrement = result.Increments.First(i => i.StartTime <= earlyTime && earlyTime < i.EndTime);
Assert.AreEqual(1, earlyIncrement.DownCount);
Assert.IsTrue(earlyIncrement.ChecksInDown.Any(c => c.CheckName == "Disabled Check"));

// The increment containing the disabled check's latest result should exclude it
var laterIncrement = result.Increments.First(i => i.StartTime <= laterTime && laterTime < i.EndTime);
Assert.AreEqual(0, laterIncrement.DownCount);
}

[TestMethod]
public async Task GetWorkspaceIncidentTimelineAsyncShouldExcludeDisabledCheckLastIncrement()
{
var disabledCheck = await CreateCheckAsync("Disabled Check", CheckTypes.Http, "60", false);

var recentTime = DateTimeOffset.UtcNow.AddSeconds(-1);
await CreateCheckResultAsync(disabledCheck.Id, CheckStatuses.Down, recentTime);

var result = await _service.GetWorkspaceIncidentTimelineAsync(_workspace.Id, 1);

Assert.IsNotNull(result);
Assert.IsNotEmpty(result.Increments);

// The increment containing the disabled check's last result should be excluded
var lastIncrement = result.Increments.Last();
Assert.AreEqual(0, lastIncrement.TotalChecks);
Assert.AreEqual(0, lastIncrement.DownCount);
}

[TestMethod]
public async Task GetWorkspaceIncidentTimelineAsyncShouldExcludeDisabledCheckWithNoResults()
{
var enabledCheck = await CreateCheckAsync("Enabled Check", CheckTypes.Http, "60", true);
await CreateCheckAsync("Disabled No Results", CheckTypes.Http, "60", false);

var recentTime = DateTimeOffset.UtcNow.AddSeconds(-1);
await CreateCheckResultAsync(enabledCheck.Id, CheckStatuses.Up, recentTime);

var result = await _service.GetWorkspaceIncidentTimelineAsync(_workspace.Id, 1);

Assert.IsNotNull(result);
Assert.IsNotEmpty(result.Increments);

// Disabled check with no results should not inflate TotalChecks
var lastIncrement = result.Increments.Last();
Assert.AreEqual(1, lastIncrement.TotalChecks);
Assert.AreEqual(1, lastIncrement.UpCount);
Assert.AreEqual(0, lastIncrement.DownCount);
}

[TestMethod]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public void DetectChangesShouldDetectTriggerOnWarnChange()
oldAlert.Enabled,
[]);

Assert.IsTrue(changes.ContainsKey("Alert 'Test': Trigger on Warn"));
Assert.IsTrue(changes.ContainsKey("Alert 'Test': Trigger on Degraded"));
Assert.IsTrue(changes.ContainsKey("Alert 'Test': Updated At"));
}

Expand Down Expand Up @@ -293,7 +293,7 @@ public void DetectChangesShouldHandleMultipleChanges()
[Guid.NewGuid()]);

Assert.IsTrue(changes.ContainsKey("Alert 'Old Alert' (renamed to 'New Alert'): Name"));
Assert.IsTrue(changes.ContainsKey("Alert 'Old Alert' (renamed to 'New Alert'): Trigger on Warn"));
Assert.IsTrue(changes.ContainsKey("Alert 'Old Alert' (renamed to 'New Alert'): Trigger on Degraded"));
Assert.IsTrue(changes.ContainsKey("Alert 'Old Alert' (renamed to 'New Alert'): Trigger on Down"));
Assert.IsTrue(changes.ContainsKey("Alert 'Old Alert' (renamed to 'New Alert'): Failure Threshold"));
Assert.IsTrue(changes.ContainsKey("Alert 'Old Alert' (renamed to 'New Alert'): Send Recovery Notification"));
Expand Down Expand Up @@ -322,7 +322,7 @@ public void BuildCreationInfoShouldIncludeTriggerOnWarnOnly()
var info = _service.BuildCreationInfo("Test Alert", true, false, 1, true, true, []);

Assert.IsTrue(info.ContainsKey("Triggers"));
Assert.AreEqual("Warn", info["Triggers"]);
Assert.AreEqual("Degraded", info["Triggers"]);
}

[TestMethod]
Expand All @@ -340,7 +340,7 @@ public void BuildCreationInfoShouldIncludeBothTriggers()
var info = _service.BuildCreationInfo("Test Alert", true, true, 1, true, true, []);

Assert.IsTrue(info.ContainsKey("Triggers"));
Assert.AreEqual("Warn, Down", info["Triggers"]);
Assert.AreEqual("Degraded, Down", info["Triggers"]);
}

[TestMethod]
Expand Down
2 changes: 1 addition & 1 deletion SAMA.Web/Pages/Alerts/Create.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
<div class="form-check">
<input asp-for="Input.TriggerOnWarn" class="form-check-input" type="checkbox" />
<label asp-for="Input.TriggerOnWarn" class="form-check-label">
<span class="badge bg-warning text-dark">Warn</span> Warning - Check degraded but not failed
<span class="badge bg-warning text-dark">Degraded</span> Check degraded but not failed
</label>
</div>
<div class="form-check">
Expand Down
2 changes: 1 addition & 1 deletion SAMA.Web/Pages/Alerts/Delete.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
<dd class="col-sm-8">
@if (Model.AlertToDelete.TriggerOnWarn)
{
<span class="badge bg-warning text-dark">Warn</span>
<span class="badge bg-warning text-dark">Degraded</span>
}
@if (Model.AlertToDelete.TriggerOnDown)
{
Expand Down
2 changes: 1 addition & 1 deletion SAMA.Web/Pages/Alerts/Details.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
<dd class="col-sm-9">
@if (Model.Alert.TriggerOnWarn)
{
<span class="badge bg-warning text-dark">Warn</span>
<span class="badge bg-warning text-dark">Degraded</span>
}
@if (Model.Alert.TriggerOnDown)
{
Expand Down
2 changes: 1 addition & 1 deletion SAMA.Web/Pages/Alerts/Edit.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
<div class="form-check">
<input asp-for="Input.TriggerOnWarn" class="form-check-input" type="checkbox" />
<label asp-for="Input.TriggerOnWarn" class="form-check-label">
<span class="badge bg-warning text-dark">Warn</span> Warning - Check degraded but not failed
<span class="badge bg-warning text-dark">Degraded</span> Check degraded but not failed
</label>
</div>
<div class="form-check">
Expand Down
6 changes: 3 additions & 3 deletions SAMA.Web/Pages/Alerts/Index.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
{
<div class="alert alert-info">
<h5 class="alert-heading">No Alert Rules Configured</h5>
<p>Create your first alert rule to start receiving notifications when this check fails or enters a warning state.</p>
<p>Create your first alert rule to start receiving notifications when this check fails or enters a degraded state.</p>
@if (canEdit)
{
<a asp-page="Create" asp-route-checkId="@Model.CheckId" class="btn btn-primary">
Expand All @@ -43,7 +43,7 @@ else
<tr>
<th class="text-nowrap">Name</th>
<th class="text-nowrap">
Triggers <i class="icon-circle-question-mark icon-xs text-muted" tabindex="0" data-bs-toggle="popover" data-bs-placement="top" data-bs-trigger="hover focus" data-bs-content="Which check states (Warning/Down) will trigger this alert rule"></i>
Triggers <i class="icon-circle-question-mark icon-xs text-muted" tabindex="0" data-bs-toggle="popover" data-bs-placement="top" data-bs-trigger="hover focus" data-bs-content="Which check states (Degraded/Down) will trigger this alert rule"></i>
</th>
<th class="text-nowrap">
Threshold <i class="icon-circle-question-mark icon-xs text-muted" tabindex="0" data-bs-toggle="popover" data-bs-placement="top" data-bs-trigger="hover focus" data-bs-content="Number of consecutive failures required before sending a notification"></i>
Expand Down Expand Up @@ -73,7 +73,7 @@ else
<td class="text-nowrap">
@if (alert.TriggerOnWarn)
{
<span class="badge bg-warning text-dark">Warn</span>
<span class="badge bg-warning text-dark">Degraded</span>
}
@if (alert.TriggerOnDown)
{
Expand Down
6 changes: 3 additions & 3 deletions SAMA.Web/Pages/Checks/Details.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
<div class="alert alert-warning mb-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<strong>Current Status:</strong> ⚠ Warning - degraded performance
<strong>Current Status:</strong> ⚠ Degraded performance
</div>
<div>
<small class="text-muted">
Expand Down Expand Up @@ -300,7 +300,7 @@
<tr>
<th class="text-nowrap">Name</th>
<th class="text-nowrap">
Triggers <i class="icon-circle-question-mark icon-xs text-muted" tabindex="0" data-bs-toggle="popover" data-bs-placement="top" data-bs-trigger="hover focus" data-bs-content="Which check states (Warning/Down) will trigger this alert rule"></i>
Triggers <i class="icon-circle-question-mark icon-xs text-muted" tabindex="0" data-bs-toggle="popover" data-bs-placement="top" data-bs-trigger="hover focus" data-bs-content="Which check states (Degraded/Down) will trigger this alert rule"></i>
</th>
<th class="text-nowrap">
Threshold <i class="icon-circle-question-mark icon-xs text-muted" tabindex="0" data-bs-toggle="popover" data-bs-placement="top" data-bs-trigger="hover focus" data-bs-content="Number of consecutive failures required before sending a notification"></i>
Expand All @@ -322,7 +322,7 @@
<td class="text-nowrap">
@if (alert.TriggerOnWarn)
{
<span class="badge bg-warning text-dark me-1">Warn</span>
<span class="badge bg-warning text-dark me-1">Degraded</span>
}
@if (alert.TriggerOnDown)
{
Expand Down
2 changes: 1 addition & 1 deletion SAMA.Web/Pages/Checks/Index.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
}
else if (check.LastStatus == CheckStatuses.Warn)
{
<span class="badge bg-warning text-dark">Warning</span>
<span class="badge bg-warning text-dark">Degraded</span>
}
else if (check.LastStatus == CheckStatuses.Down)
{
Expand Down
2 changes: 1 addition & 1 deletion SAMA.Web/Pages/Checks/_HttpConfigDisplay.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
</dd>
}

<dt class="col-sm-4">Response Time Warn Threshold</dt>
<dt class="col-sm-4">Response Time Degraded Threshold</dt>
<dd class="col-sm-8">
@if (!string.IsNullOrWhiteSpace(Model.GetValueOrDefault(ConfigurationKeys.HttpCheck.ResponseTimeWarnThresholdMs)?.ToString()))
{
Expand Down
4 changes: 2 additions & 2 deletions SAMA.Web/Pages/Checks/_HttpConfigFields.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@
</div>

<div class="mb-3">
<label for="Input_HttpResponseTimeWarnThresholdMs" class="form-label">Response Time Warning Threshold (optional)</label>
<label for="Input_HttpResponseTimeWarnThresholdMs" class="form-label">Response Time Degraded Threshold (optional)</label>
<input asp-for="HttpResponseTimeWarnThresholdMs" id="Input_HttpResponseTimeWarnThresholdMs" name="Input.HttpResponseTimeWarnThresholdMs" class="form-control" type="number" min="1" placeholder="2000" />
<span asp-validation-for="HttpResponseTimeWarnThresholdMs" class="text-danger"></span>
<div class="form-text">If response time exceeds this value (in milliseconds), mark as <strong>Warn</strong> instead of <strong>Up</strong>. Leave blank to disable.</div>
<div class="form-text">If response time exceeds this value (in milliseconds), mark as <strong>Degraded</strong> instead of <strong>Up</strong>. Leave blank to disable.</div>
</div>

<div class="mb-3 form-check">
Expand Down
2 changes: 1 addition & 1 deletion SAMA.Web/Pages/Checks/_PingConfigDisplay.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<dt class="col-sm-4">Packet Count</dt>
<dd class="col-sm-8">@Model.GetValueOrDefault(ConfigurationKeys.PingCheck.PacketCount)</dd>

<dt class="col-sm-4">Packet Loss Warning Threshold</dt>
<dt class="col-sm-4">Packet Loss Degraded Threshold</dt>
<dd class="col-sm-8 mb-0">@Model.GetValueOrDefault(ConfigurationKeys.PingCheck.PacketLossThresholdPercent)%</dd>
</dl>
</div>
6 changes: 3 additions & 3 deletions SAMA.Web/Pages/Checks/_PingConfigFields.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<small><strong>Note:</strong> This check sends ICMP echo requests (ping) to the specified host.</small>
<ul class="mb-0 mt-1 small">
<li><strong>Up:</strong> Packet loss is below the threshold</li>
<li><strong>Warn:</strong> Packet loss is at or above the threshold, but at least one packet succeeded</li>
<li><strong>Degraded:</strong> Packet loss is at or above the threshold, but at least one packet succeeded</li>
<li><strong>Down:</strong> All packets failed</li>
</ul>
<small><em>Note: ICMP may require elevated privileges on some systems.</em></small>
Expand All @@ -29,9 +29,9 @@
</div>

<div class="mb-3">
<label for="Input_PingPacketLossThresholdPercent" class="form-label">Packet Loss Warning Threshold (%)</label>
<label for="Input_PingPacketLossThresholdPercent" class="form-label">Packet Loss Degraded Threshold (%)</label>
<input asp-for="PingPacketLossThresholdPercent" id="Input_PingPacketLossThresholdPercent" name="Input.PingPacketLossThresholdPercent" class="form-control" type="number" min="0" max="100" />
<span asp-validation-for="PingPacketLossThresholdPercent" class="text-danger"></span>
<div class="form-text">Packet loss percentage that triggers a <strong>Warn</strong> status (0-100). Default is @CheckDefaults.PingPacketLossThresholdPercent%.</div>
<div class="form-text">Packet loss percentage that triggers a <strong>Degraded</strong> status (0-100). Default is @CheckDefaults.PingPacketLossThresholdPercent%.</div>
</div>
</div>
2 changes: 1 addition & 1 deletion SAMA.Web/Pages/Checks/_TcpConfigDisplay.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<dt class="col-sm-4">Port</dt>
<dd class="col-sm-8">@Model.GetValueOrDefault(ConfigurationKeys.TcpCheck.Port)</dd>

<dt class="col-sm-4">Connection Time Warn Threshold</dt>
<dt class="col-sm-4">Connection Time Degraded Threshold</dt>
<dd class="col-sm-8 mb-0">
@if (!string.IsNullOrWhiteSpace(Model.GetValueOrDefault(ConfigurationKeys.TcpCheck.ConnectionTimeWarnThresholdMs)?.ToString()))
{
Expand Down
4 changes: 2 additions & 2 deletions SAMA.Web/Pages/Checks/_TcpConfigFields.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
</div>

<div class="mb-3">
<label for="Input_TcpConnectionTimeWarnThresholdMs" class="form-label">Connection Time Warning Threshold (optional)</label>
<label for="Input_TcpConnectionTimeWarnThresholdMs" class="form-label">Connection Time Degraded Threshold (optional)</label>
<input asp-for="TcpConnectionTimeWarnThresholdMs" id="Input_TcpConnectionTimeWarnThresholdMs" name="Input.TcpConnectionTimeWarnThresholdMs" class="form-control" type="number" min="1" placeholder="1000" />
<span asp-validation-for="TcpConnectionTimeWarnThresholdMs" class="text-danger"></span>
<div class="form-text">If connection time exceeds this value (in milliseconds), mark as <strong>Warn</strong> instead of <strong>Up</strong>. Leave blank to disable.</div>
<div class="form-text">If connection time exceeds this value (in milliseconds), mark as <strong>Degraded</strong> instead of <strong>Up</strong>. Leave blank to disable.</div>
</div>
</div>
2 changes: 1 addition & 1 deletion SAMA.Web/Pages/Checks/_TlsConfigDisplay.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<code class="text-bg-secondary">@Model.GetValueOrDefault(ConfigurationKeys.TlsCheck.Url)</code>
</dd>

<dt class="col-sm-4">Days Before Expiry Warning</dt>
<dt class="col-sm-4">Expiry Degraded Threshold (Days)</dt>
<dd class="col-sm-8">@Model.GetValueOrDefault(ConfigurationKeys.TlsCheck.DaysBeforeExpiryWarning)</dd>

<dt class="col-sm-4">Custom CA Certificate</dt>
Expand Down
Loading
Loading