183 lines
6.3 KiB
C#
183 lines
6.3 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
using StellaOps.Notify.Engine;
|
|
using StellaOps.Notify.Models;
|
|
using StellaOps.Notify.WebService.Contracts;
|
|
|
|
namespace StellaOps.Notify.WebService.Services;
|
|
|
|
internal interface INotifyChannelHealthService
|
|
{
|
|
Task<ChannelHealthResponse> CheckAsync(
|
|
string tenantId,
|
|
NotifyChannel channel,
|
|
string traceId,
|
|
CancellationToken cancellationToken);
|
|
}
|
|
|
|
internal sealed class NotifyChannelHealthService : INotifyChannelHealthService
|
|
{
|
|
private readonly TimeProvider _timeProvider;
|
|
private readonly ILogger<NotifyChannelHealthService> _logger;
|
|
private readonly IReadOnlyDictionary<NotifyChannelType, INotifyChannelHealthProvider> _providers;
|
|
|
|
public NotifyChannelHealthService(
|
|
TimeProvider timeProvider,
|
|
ILogger<NotifyChannelHealthService> logger,
|
|
IEnumerable<INotifyChannelHealthProvider> providers)
|
|
{
|
|
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
_providers = BuildProviderMap(providers ?? Array.Empty<INotifyChannelHealthProvider>(), _logger);
|
|
}
|
|
|
|
public async Task<ChannelHealthResponse> CheckAsync(
|
|
string tenantId,
|
|
NotifyChannel channel,
|
|
string traceId,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(channel);
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
var target = ResolveTarget(channel);
|
|
var timestamp = _timeProvider.GetUtcNow();
|
|
var context = new ChannelHealthContext(
|
|
tenantId,
|
|
channel,
|
|
target,
|
|
timestamp,
|
|
traceId);
|
|
|
|
ChannelHealthResult? providerResult = null;
|
|
var providerName = "fallback";
|
|
|
|
if (_providers.TryGetValue(channel.Type, out var provider))
|
|
{
|
|
try
|
|
{
|
|
providerResult = await provider.CheckAsync(context, cancellationToken).ConfigureAwait(false);
|
|
providerName = provider.GetType().FullName ?? provider.GetType().Name;
|
|
}
|
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
|
{
|
|
throw;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(
|
|
ex,
|
|
"Notify channel health provider {Provider} failed for tenant {TenantId}, channel {ChannelId} ({ChannelType}).",
|
|
provider.GetType().FullName,
|
|
tenantId,
|
|
channel.ChannelId,
|
|
channel.Type);
|
|
providerResult = new ChannelHealthResult(
|
|
ChannelHealthStatus.Degraded,
|
|
"Channel health provider threw an exception. See logs for details.",
|
|
new Dictionary<string, string>(StringComparer.Ordinal));
|
|
}
|
|
}
|
|
|
|
var metadata = MergeMetadata(context, providerName, providerResult?.Metadata);
|
|
var status = providerResult?.Status ?? ChannelHealthStatus.Healthy;
|
|
var message = providerResult?.Message ?? "Channel metadata returned without provider-specific diagnostics.";
|
|
|
|
var response = new ChannelHealthResponse(
|
|
tenantId,
|
|
channel.ChannelId,
|
|
status,
|
|
message,
|
|
timestamp,
|
|
traceId,
|
|
metadata);
|
|
|
|
_logger.LogInformation(
|
|
"Notify channel health generated for tenant {TenantId}, channel {ChannelId} ({ChannelType}) using provider {Provider}.",
|
|
tenantId,
|
|
channel.ChannelId,
|
|
channel.Type,
|
|
providerName);
|
|
|
|
return response;
|
|
}
|
|
|
|
private static IReadOnlyDictionary<NotifyChannelType, INotifyChannelHealthProvider> BuildProviderMap(
|
|
IEnumerable<INotifyChannelHealthProvider> providers,
|
|
ILogger logger)
|
|
{
|
|
var map = new Dictionary<NotifyChannelType, INotifyChannelHealthProvider>();
|
|
foreach (var provider in providers)
|
|
{
|
|
if (provider is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (map.TryGetValue(provider.ChannelType, out var existing))
|
|
{
|
|
logger?.LogWarning(
|
|
"Multiple Notify channel health providers registered for {ChannelType}. Keeping {ExistingProvider} and ignoring {NewProvider}.",
|
|
provider.ChannelType,
|
|
existing.GetType().FullName,
|
|
provider.GetType().FullName);
|
|
continue;
|
|
}
|
|
|
|
map[provider.ChannelType] = provider;
|
|
}
|
|
|
|
return map;
|
|
}
|
|
|
|
private static string ResolveTarget(NotifyChannel channel)
|
|
{
|
|
var target = channel.Config.Target ?? channel.Config.Endpoint;
|
|
if (string.IsNullOrWhiteSpace(target))
|
|
{
|
|
return channel.Name;
|
|
}
|
|
|
|
return target;
|
|
}
|
|
|
|
private static IReadOnlyDictionary<string, string> MergeMetadata(
|
|
ChannelHealthContext context,
|
|
string providerName,
|
|
IReadOnlyDictionary<string, string>? providerMetadata)
|
|
{
|
|
var metadata = new Dictionary<string, string>(StringComparer.Ordinal)
|
|
{
|
|
["channelType"] = context.Channel.Type.ToString().ToLowerInvariant(),
|
|
["target"] = context.Target,
|
|
["previewProvider"] = providerName,
|
|
["traceId"] = context.TraceId,
|
|
["channelEnabled"] = context.Channel.Enabled.ToString()
|
|
};
|
|
|
|
foreach (var label in context.Channel.Labels)
|
|
{
|
|
metadata[$"label.{label.Key}"] = label.Value;
|
|
}
|
|
|
|
if (providerMetadata is not null)
|
|
{
|
|
foreach (var pair in providerMetadata)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(pair.Key) || pair.Value is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
metadata[pair.Key.Trim()] = pair.Value;
|
|
}
|
|
}
|
|
|
|
return metadata;
|
|
}
|
|
}
|