Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user