up the blokcing tasks
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Risk Bundle CI / risk-bundle-build (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Risk Bundle CI / risk-bundle-offline-kit (push) Has been cancelled
Risk Bundle CI / publish-checksums (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
devportal-offline / build-offline (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-12-11 02:32:18 +02:00
parent 92bc4d3a07
commit 49922dff5a
474 changed files with 76071 additions and 12411 deletions

View File

@@ -1,6 +1,7 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Notify.Models;
using StellaOps.Notifier.Worker.Channels;
namespace StellaOps.Notifier.Worker.Digest;
@@ -54,7 +55,7 @@ public sealed class DigestScheduleRunner : BackgroundService
await Task.WhenAll(scheduleTasks);
}
private async Task RunScheduleAsync(DigestSchedule schedule, CancellationToken stoppingToken)
private async Task RunScheduleAsync(DigestScheduleConfig schedule, CancellationToken stoppingToken)
{
_logger.LogInformation(
"Starting digest schedule '{Name}' with interval {Interval}.",
@@ -93,7 +94,7 @@ public sealed class DigestScheduleRunner : BackgroundService
_logger.LogInformation("Digest schedule '{Name}' stopped.", schedule.Name);
}
private async Task ExecuteScheduleAsync(DigestSchedule schedule, CancellationToken stoppingToken)
private async Task ExecuteScheduleAsync(DigestScheduleConfig schedule, CancellationToken stoppingToken)
{
var now = _timeProvider.GetUtcNow();
var query = new DigestQuery
@@ -150,7 +151,7 @@ public sealed class DigestScheduleRunner : BackgroundService
schedule.Name, successCount, errorCount, tenants.Count);
}
private TimeSpan CalculateInitialDelay(DigestSchedule schedule)
private TimeSpan CalculateInitialDelay(DigestScheduleConfig schedule)
{
if (!schedule.AlignToInterval)
{
@@ -179,7 +180,7 @@ public interface IDigestDistributor
/// </summary>
Task DistributeAsync(
DigestResult digest,
DigestSchedule schedule,
DigestScheduleConfig schedule,
CancellationToken cancellationToken = default);
}
@@ -202,48 +203,71 @@ public interface IDigestTenantProvider
public sealed class ChannelDigestDistributor : IDigestDistributor
{
private readonly IChannelAdapterFactory _channelFactory;
private readonly TimeProvider _timeProvider;
private readonly ILogger<ChannelDigestDistributor> _logger;
public ChannelDigestDistributor(
IChannelAdapterFactory channelFactory,
TimeProvider timeProvider,
ILogger<ChannelDigestDistributor> logger)
{
_channelFactory = channelFactory ?? throw new ArgumentNullException(nameof(channelFactory));
_timeProvider = timeProvider ?? TimeProvider.System;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task DistributeAsync(
DigestResult digest,
DigestSchedule schedule,
DigestScheduleConfig schedule,
CancellationToken cancellationToken = default)
{
foreach (var channelConfig in schedule.Channels)
{
try
{
var adapter = _channelFactory.Create(channelConfig.Type);
if (!Enum.TryParse<NotifyChannelType>(channelConfig.Type, true, out var channelType))
{
_logger.LogWarning("Unsupported digest channel type {ChannelType}.", channelConfig.Type);
continue;
}
var adapter = _channelFactory.GetAdapter(channelType);
if (adapter is null)
{
_logger.LogWarning("No adapter registered for digest channel {ChannelType}.", channelType);
continue;
}
var metadata = BuildMetadata(digest, schedule, channelConfig);
var channel = BuildChannel(channelType, digest, schedule, channelConfig);
var delivery = BuildDelivery(digest, channelType, metadata);
var content = SelectContent(digest, channelConfig.Type);
await adapter.SendAsync(new ChannelMessage
{
ChannelType = channelConfig.Type,
Destination = channelConfig.Destination,
Subject = $"Notification Digest - {digest.TenantId}",
Body = content,
Format = channelConfig.Format ?? GetDefaultFormat(channelConfig.Type),
Metadata = new Dictionary<string, string>
{
["digestId"] = digest.DigestId,
["tenantId"] = digest.TenantId,
["scheduleName"] = schedule.Name,
["from"] = digest.From.ToString("O"),
["to"] = digest.To.ToString("O")
}
}, cancellationToken);
var context = new ChannelDispatchContext(
delivery.DeliveryId,
digest.TenantId,
channel,
delivery,
content,
$"Notification Digest - {digest.TenantId}",
metadata,
_timeProvider.GetUtcNow(),
TraceId: $"digest-{digest.DigestId}");
_logger.LogDebug(
"Sent digest {DigestId} to channel {Channel} ({Destination}).",
digest.DigestId, channelConfig.Type, channelConfig.Destination);
var result = await adapter.DispatchAsync(context, cancellationToken).ConfigureAwait(false);
if (result.Success)
{
_logger.LogDebug(
"Sent digest {DigestId} to channel {Channel} ({Destination}).",
digest.DigestId, channelType, channelConfig.Destination);
}
else
{
_logger.LogWarning(
"Digest {DigestId} dispatch to {Channel} failed: {Message}.",
digest.DigestId, channelType, result.Message ?? "dispatch failed");
}
}
catch (Exception ex)
{
@@ -254,6 +278,77 @@ public sealed class ChannelDigestDistributor : IDigestDistributor
}
}
private static IReadOnlyDictionary<string, string> BuildMetadata(
DigestResult digest,
DigestScheduleConfig schedule,
DigestChannelConfig channelConfig)
{
return new Dictionary<string, string>(StringComparer.Ordinal)
{
["digestId"] = digest.DigestId,
["tenantId"] = digest.TenantId,
["scheduleName"] = schedule.Name,
["from"] = digest.From.ToString("O"),
["to"] = digest.To.ToString("O"),
["destination"] = channelConfig.Destination,
["channelType"] = channelConfig.Type
};
}
private static NotifyChannel BuildChannel(
NotifyChannelType channelType,
DigestResult digest,
DigestScheduleConfig schedule,
DigestChannelConfig channelConfig)
{
var properties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["destination"] = channelConfig.Destination
};
if (!string.IsNullOrWhiteSpace(channelConfig.Format))
{
properties["format"] = channelConfig.Format!;
}
var config = NotifyChannelConfig.Create(
secretRef: $"digest-{schedule.Name}",
target: channelConfig.Destination,
endpoint: channelConfig.Destination,
properties: properties);
return NotifyChannel.Create(
channelId: $"digest-{schedule.Name}-{channelType}".ToLowerInvariant(),
tenantId: digest.TenantId,
name: $"{schedule.Name}-{channelType}",
type: channelType,
config: config,
enabled: true,
metadata: properties);
}
private static NotifyDelivery BuildDelivery(
DigestResult digest,
NotifyChannelType channelType,
IReadOnlyDictionary<string, string> metadata)
{
return NotifyDelivery.Create(
deliveryId: $"digest-{digest.DigestId}-{channelType}".ToLowerInvariant(),
tenantId: digest.TenantId,
ruleId: "digest",
actionId: channelType.ToString(),
eventId: Guid.NewGuid(),
kind: "digest",
status: NotifyDeliveryStatus.Sending,
statusReason: null,
rendered: null,
attempts: Array.Empty<NotifyDeliveryAttempt>(),
metadata: metadata,
createdAt: digest.GeneratedAt,
sentAt: null,
completedAt: null);
}
private static string SelectContent(DigestResult digest, string channelType)
{
if (digest.Content is null)
@@ -269,17 +364,6 @@ public sealed class ChannelDigestDistributor : IDigestDistributor
_ => digest.Content.PlainText ?? ""
};
}
private static string GetDefaultFormat(string channelType)
{
return channelType.ToLowerInvariant() switch
{
"slack" => "blocks",
"email" => "html",
"webhook" => "json",
_ => "text"
};
}
}
/// <summary>
@@ -324,13 +408,13 @@ public sealed class DigestScheduleOptions
/// <summary>
/// Configured digest schedules.
/// </summary>
public List<DigestSchedule> Schedules { get; set; } = [];
public List<DigestScheduleConfig> Schedules { get; set; } = [];
}
/// <summary>
/// A single digest schedule configuration.
/// </summary>
public sealed class DigestSchedule
public sealed class DigestScheduleConfig
{
/// <summary>
/// Unique name for this schedule.