Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Signals Reachability Scoring & Events / reachability-smoke (push) Has been cancelled
Signals Reachability Scoring & Events / sign-and-upload (push) Has been cancelled
402 lines
12 KiB
C#
402 lines
12 KiB
C#
using System.Collections.Immutable;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace StellaOps.Notify.Models;
|
|
|
|
/// <summary>
|
|
/// Quiet hours schedule configuration for suppressing notifications during specified periods.
|
|
/// </summary>
|
|
public sealed record NotifyQuietHoursSchedule
|
|
{
|
|
[JsonConstructor]
|
|
public NotifyQuietHoursSchedule(
|
|
string scheduleId,
|
|
string tenantId,
|
|
string name,
|
|
string cronExpression,
|
|
TimeSpan duration,
|
|
string timeZone,
|
|
string? channelId = null,
|
|
bool enabled = true,
|
|
string? description = null,
|
|
ImmutableDictionary<string, string>? metadata = null,
|
|
string? createdBy = null,
|
|
DateTimeOffset? createdAt = null,
|
|
string? updatedBy = null,
|
|
DateTimeOffset? updatedAt = null)
|
|
{
|
|
ScheduleId = NotifyValidation.EnsureNotNullOrWhiteSpace(scheduleId, nameof(scheduleId));
|
|
TenantId = NotifyValidation.EnsureNotNullOrWhiteSpace(tenantId, nameof(tenantId));
|
|
Name = NotifyValidation.EnsureNotNullOrWhiteSpace(name, nameof(name));
|
|
CronExpression = NotifyValidation.EnsureNotNullOrWhiteSpace(cronExpression, nameof(cronExpression));
|
|
Duration = duration > TimeSpan.Zero ? duration : TimeSpan.FromHours(8);
|
|
TimeZone = NotifyValidation.EnsureNotNullOrWhiteSpace(timeZone, nameof(timeZone));
|
|
ChannelId = NotifyValidation.TrimToNull(channelId);
|
|
Enabled = enabled;
|
|
Description = NotifyValidation.TrimToNull(description);
|
|
Metadata = NotifyValidation.NormalizeStringDictionary(metadata);
|
|
CreatedBy = NotifyValidation.TrimToNull(createdBy);
|
|
CreatedAt = NotifyValidation.EnsureUtc(createdAt ?? DateTimeOffset.UtcNow);
|
|
UpdatedBy = NotifyValidation.TrimToNull(updatedBy);
|
|
UpdatedAt = NotifyValidation.EnsureUtc(updatedAt ?? CreatedAt);
|
|
}
|
|
|
|
public static NotifyQuietHoursSchedule Create(
|
|
string scheduleId,
|
|
string tenantId,
|
|
string name,
|
|
string cronExpression,
|
|
TimeSpan duration,
|
|
string timeZone,
|
|
string? channelId = null,
|
|
bool enabled = true,
|
|
string? description = null,
|
|
IEnumerable<KeyValuePair<string, string>>? metadata = null,
|
|
string? createdBy = null,
|
|
DateTimeOffset? createdAt = null,
|
|
string? updatedBy = null,
|
|
DateTimeOffset? updatedAt = null)
|
|
{
|
|
return new NotifyQuietHoursSchedule(
|
|
scheduleId,
|
|
tenantId,
|
|
name,
|
|
cronExpression,
|
|
duration,
|
|
timeZone,
|
|
channelId,
|
|
enabled,
|
|
description,
|
|
ToImmutableDictionary(metadata),
|
|
createdBy,
|
|
createdAt,
|
|
updatedBy,
|
|
updatedAt);
|
|
}
|
|
|
|
public string ScheduleId { get; }
|
|
|
|
public string TenantId { get; }
|
|
|
|
public string Name { get; }
|
|
|
|
/// <summary>
|
|
/// Cron expression defining when quiet hours start.
|
|
/// </summary>
|
|
public string CronExpression { get; }
|
|
|
|
/// <summary>
|
|
/// Duration of the quiet hours window.
|
|
/// </summary>
|
|
public TimeSpan Duration { get; }
|
|
|
|
/// <summary>
|
|
/// IANA time zone for evaluating the cron expression (e.g., "America/New_York").
|
|
/// </summary>
|
|
public string TimeZone { get; }
|
|
|
|
/// <summary>
|
|
/// Optional channel ID to scope quiet hours to a specific channel.
|
|
/// If null, applies to all channels.
|
|
/// </summary>
|
|
public string? ChannelId { get; }
|
|
|
|
public bool Enabled { get; }
|
|
|
|
public string? Description { get; }
|
|
|
|
public ImmutableDictionary<string, string> Metadata { get; }
|
|
|
|
public string? CreatedBy { get; }
|
|
|
|
public DateTimeOffset CreatedAt { get; }
|
|
|
|
public string? UpdatedBy { get; }
|
|
|
|
public DateTimeOffset UpdatedAt { get; }
|
|
|
|
private static ImmutableDictionary<string, string>? ToImmutableDictionary(IEnumerable<KeyValuePair<string, string>>? pairs)
|
|
{
|
|
if (pairs is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var builder = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
|
foreach (var (key, value) in pairs)
|
|
{
|
|
builder[key] = value;
|
|
}
|
|
|
|
return builder.ToImmutable();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maintenance window for planned suppression of notifications.
|
|
/// </summary>
|
|
public sealed record NotifyMaintenanceWindow
|
|
{
|
|
[JsonConstructor]
|
|
public NotifyMaintenanceWindow(
|
|
string windowId,
|
|
string tenantId,
|
|
string name,
|
|
DateTimeOffset startsAt,
|
|
DateTimeOffset endsAt,
|
|
bool suppressNotifications = true,
|
|
string? reason = null,
|
|
ImmutableArray<string> channelIds = default,
|
|
ImmutableArray<string> ruleIds = default,
|
|
ImmutableDictionary<string, string>? metadata = null,
|
|
string? createdBy = null,
|
|
DateTimeOffset? createdAt = null,
|
|
string? updatedBy = null,
|
|
DateTimeOffset? updatedAt = null)
|
|
{
|
|
WindowId = NotifyValidation.EnsureNotNullOrWhiteSpace(windowId, nameof(windowId));
|
|
TenantId = NotifyValidation.EnsureNotNullOrWhiteSpace(tenantId, nameof(tenantId));
|
|
Name = NotifyValidation.EnsureNotNullOrWhiteSpace(name, nameof(name));
|
|
StartsAt = NotifyValidation.EnsureUtc(startsAt);
|
|
EndsAt = NotifyValidation.EnsureUtc(endsAt);
|
|
SuppressNotifications = suppressNotifications;
|
|
Reason = NotifyValidation.TrimToNull(reason);
|
|
ChannelIds = NormalizeStringArray(channelIds);
|
|
RuleIds = NormalizeStringArray(ruleIds);
|
|
Metadata = NotifyValidation.NormalizeStringDictionary(metadata);
|
|
CreatedBy = NotifyValidation.TrimToNull(createdBy);
|
|
CreatedAt = NotifyValidation.EnsureUtc(createdAt ?? DateTimeOffset.UtcNow);
|
|
UpdatedBy = NotifyValidation.TrimToNull(updatedBy);
|
|
UpdatedAt = NotifyValidation.EnsureUtc(updatedAt ?? CreatedAt);
|
|
|
|
if (EndsAt <= StartsAt)
|
|
{
|
|
throw new ArgumentException("EndsAt must be after StartsAt.", nameof(endsAt));
|
|
}
|
|
}
|
|
|
|
public static NotifyMaintenanceWindow Create(
|
|
string windowId,
|
|
string tenantId,
|
|
string name,
|
|
DateTimeOffset startsAt,
|
|
DateTimeOffset endsAt,
|
|
bool suppressNotifications = true,
|
|
string? reason = null,
|
|
IEnumerable<string>? channelIds = null,
|
|
IEnumerable<string>? ruleIds = null,
|
|
IEnumerable<KeyValuePair<string, string>>? metadata = null,
|
|
string? createdBy = null,
|
|
DateTimeOffset? createdAt = null,
|
|
string? updatedBy = null,
|
|
DateTimeOffset? updatedAt = null)
|
|
{
|
|
return new NotifyMaintenanceWindow(
|
|
windowId,
|
|
tenantId,
|
|
name,
|
|
startsAt,
|
|
endsAt,
|
|
suppressNotifications,
|
|
reason,
|
|
ToImmutableArray(channelIds),
|
|
ToImmutableArray(ruleIds),
|
|
ToImmutableDictionary(metadata),
|
|
createdBy,
|
|
createdAt,
|
|
updatedBy,
|
|
updatedAt);
|
|
}
|
|
|
|
public string WindowId { get; }
|
|
|
|
public string TenantId { get; }
|
|
|
|
public string Name { get; }
|
|
|
|
public DateTimeOffset StartsAt { get; }
|
|
|
|
public DateTimeOffset EndsAt { get; }
|
|
|
|
/// <summary>
|
|
/// Whether to suppress notifications during the maintenance window.
|
|
/// </summary>
|
|
public bool SuppressNotifications { get; }
|
|
|
|
/// <summary>
|
|
/// Reason for the maintenance window.
|
|
/// </summary>
|
|
public string? Reason { get; }
|
|
|
|
/// <summary>
|
|
/// Optional list of channel IDs to scope the maintenance window.
|
|
/// If empty, applies to all channels.
|
|
/// </summary>
|
|
public ImmutableArray<string> ChannelIds { get; }
|
|
|
|
/// <summary>
|
|
/// Optional list of rule IDs to scope the maintenance window.
|
|
/// If empty, applies to all rules.
|
|
/// </summary>
|
|
public ImmutableArray<string> RuleIds { get; }
|
|
|
|
public ImmutableDictionary<string, string> Metadata { get; }
|
|
|
|
public string? CreatedBy { get; }
|
|
|
|
public DateTimeOffset CreatedAt { get; }
|
|
|
|
public string? UpdatedBy { get; }
|
|
|
|
public DateTimeOffset UpdatedAt { get; }
|
|
|
|
/// <summary>
|
|
/// Checks if the maintenance window is active at the specified time.
|
|
/// </summary>
|
|
public bool IsActiveAt(DateTimeOffset timestamp)
|
|
=> SuppressNotifications && timestamp >= StartsAt && timestamp < EndsAt;
|
|
|
|
private static ImmutableArray<string> NormalizeStringArray(ImmutableArray<string> values)
|
|
{
|
|
if (values.IsDefaultOrEmpty)
|
|
{
|
|
return ImmutableArray<string>.Empty;
|
|
}
|
|
|
|
return values
|
|
.Where(static v => !string.IsNullOrWhiteSpace(v))
|
|
.Select(static v => v.Trim())
|
|
.Distinct(StringComparer.Ordinal)
|
|
.ToImmutableArray();
|
|
}
|
|
|
|
private static ImmutableArray<string> ToImmutableArray(IEnumerable<string>? values)
|
|
=> values is null ? ImmutableArray<string>.Empty : values.ToImmutableArray();
|
|
|
|
private static ImmutableDictionary<string, string>? ToImmutableDictionary(IEnumerable<KeyValuePair<string, string>>? pairs)
|
|
{
|
|
if (pairs is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var builder = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal);
|
|
foreach (var (key, value) in pairs)
|
|
{
|
|
builder[key] = value;
|
|
}
|
|
|
|
return builder.ToImmutable();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Operator override for quiet hours or throttle configuration.
|
|
/// Allows an operator to temporarily bypass quiet hours or throttling.
|
|
/// </summary>
|
|
public sealed record NotifyOperatorOverride
|
|
{
|
|
[JsonConstructor]
|
|
public NotifyOperatorOverride(
|
|
string overrideId,
|
|
string tenantId,
|
|
NotifyOverrideType overrideType,
|
|
DateTimeOffset expiresAt,
|
|
string? channelId = null,
|
|
string? ruleId = null,
|
|
string? reason = null,
|
|
string? createdBy = null,
|
|
DateTimeOffset? createdAt = null)
|
|
{
|
|
OverrideId = NotifyValidation.EnsureNotNullOrWhiteSpace(overrideId, nameof(overrideId));
|
|
TenantId = NotifyValidation.EnsureNotNullOrWhiteSpace(tenantId, nameof(tenantId));
|
|
OverrideType = overrideType;
|
|
ExpiresAt = NotifyValidation.EnsureUtc(expiresAt);
|
|
ChannelId = NotifyValidation.TrimToNull(channelId);
|
|
RuleId = NotifyValidation.TrimToNull(ruleId);
|
|
Reason = NotifyValidation.TrimToNull(reason);
|
|
CreatedBy = NotifyValidation.TrimToNull(createdBy);
|
|
CreatedAt = NotifyValidation.EnsureUtc(createdAt ?? DateTimeOffset.UtcNow);
|
|
}
|
|
|
|
public static NotifyOperatorOverride Create(
|
|
string overrideId,
|
|
string tenantId,
|
|
NotifyOverrideType overrideType,
|
|
DateTimeOffset expiresAt,
|
|
string? channelId = null,
|
|
string? ruleId = null,
|
|
string? reason = null,
|
|
string? createdBy = null,
|
|
DateTimeOffset? createdAt = null)
|
|
{
|
|
return new NotifyOperatorOverride(
|
|
overrideId,
|
|
tenantId,
|
|
overrideType,
|
|
expiresAt,
|
|
channelId,
|
|
ruleId,
|
|
reason,
|
|
createdBy,
|
|
createdAt);
|
|
}
|
|
|
|
public string OverrideId { get; }
|
|
|
|
public string TenantId { get; }
|
|
|
|
public NotifyOverrideType OverrideType { get; }
|
|
|
|
public DateTimeOffset ExpiresAt { get; }
|
|
|
|
/// <summary>
|
|
/// Optional channel ID to scope the override.
|
|
/// </summary>
|
|
public string? ChannelId { get; }
|
|
|
|
/// <summary>
|
|
/// Optional rule ID to scope the override.
|
|
/// </summary>
|
|
public string? RuleId { get; }
|
|
|
|
public string? Reason { get; }
|
|
|
|
public string? CreatedBy { get; }
|
|
|
|
public DateTimeOffset CreatedAt { get; }
|
|
|
|
/// <summary>
|
|
/// Checks if the override is active at the specified time.
|
|
/// </summary>
|
|
public bool IsActiveAt(DateTimeOffset timestamp)
|
|
=> timestamp < ExpiresAt;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Type of operator override.
|
|
/// </summary>
|
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
|
public enum NotifyOverrideType
|
|
{
|
|
/// <summary>
|
|
/// Bypass quiet hours.
|
|
/// </summary>
|
|
BypassQuietHours,
|
|
|
|
/// <summary>
|
|
/// Bypass throttling.
|
|
/// </summary>
|
|
BypassThrottle,
|
|
|
|
/// <summary>
|
|
/// Bypass maintenance window.
|
|
/// </summary>
|
|
BypassMaintenance,
|
|
|
|
/// <summary>
|
|
/// Force suppress notifications.
|
|
/// </summary>
|
|
ForceSuppression
|
|
}
|