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
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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user