audit notes work completed, test fixes work (95% done), new sprints, new data sources setup and configuration
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Notify.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates that at least one notification channel is configured.
|
||||
/// </summary>
|
||||
public sealed class NotifyChannelConfigurationCheck : IDoctorCheck
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.notify.channel.configured";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Notification Channel Configuration";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Validates that at least one notification channel is configured";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Info;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["notify", "channel", "configuration"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromMilliseconds(50);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.notify", DoctorCategory.Notify.ToString());
|
||||
|
||||
var configuredChannels = new List<string>();
|
||||
var issues = new List<string>();
|
||||
|
||||
// Check Email channel configuration
|
||||
var emailEnabled = context.Configuration.GetValue<bool?>("Notify:Channels:Email:Enabled");
|
||||
var emailSection = context.Configuration.GetSection("Notify:Channels:Email");
|
||||
if (emailEnabled == true || emailSection.Exists())
|
||||
{
|
||||
var smtpHost = context.Configuration.GetValue<string>("Notify:Channels:Email:SmtpHost");
|
||||
if (string.IsNullOrWhiteSpace(smtpHost))
|
||||
{
|
||||
issues.Add("Email channel enabled but SMTP host not configured");
|
||||
}
|
||||
else
|
||||
{
|
||||
configuredChannels.Add("Email");
|
||||
}
|
||||
}
|
||||
|
||||
// Check Slack channel configuration
|
||||
var slackEnabled = context.Configuration.GetValue<bool?>("Notify:Channels:Slack:Enabled");
|
||||
var slackSection = context.Configuration.GetSection("Notify:Channels:Slack");
|
||||
if (slackEnabled == true || slackSection.Exists())
|
||||
{
|
||||
var webhookUrl = context.Configuration.GetValue<string>("Notify:Channels:Slack:WebhookUrl");
|
||||
var token = context.Configuration.GetValue<string>("Notify:Channels:Slack:Token");
|
||||
if (string.IsNullOrWhiteSpace(webhookUrl) && string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
issues.Add("Slack channel enabled but no webhook URL or token configured");
|
||||
}
|
||||
else
|
||||
{
|
||||
configuredChannels.Add("Slack");
|
||||
}
|
||||
}
|
||||
|
||||
// Check Teams channel configuration
|
||||
var teamsEnabled = context.Configuration.GetValue<bool?>("Notify:Channels:Teams:Enabled");
|
||||
var teamsSection = context.Configuration.GetSection("Notify:Channels:Teams");
|
||||
if (teamsEnabled == true || teamsSection.Exists())
|
||||
{
|
||||
var webhookUrl = context.Configuration.GetValue<string>("Notify:Channels:Teams:WebhookUrl");
|
||||
if (string.IsNullOrWhiteSpace(webhookUrl))
|
||||
{
|
||||
issues.Add("Teams channel enabled but webhook URL not configured");
|
||||
}
|
||||
else
|
||||
{
|
||||
configuredChannels.Add("Teams");
|
||||
}
|
||||
}
|
||||
|
||||
// Check Webhook channel configuration
|
||||
var webhookEnabled = context.Configuration.GetValue<bool?>("Notify:Channels:Webhook:Enabled");
|
||||
var webhookSection = context.Configuration.GetSection("Notify:Channels:Webhook");
|
||||
if (webhookEnabled == true || webhookSection.Exists())
|
||||
{
|
||||
var endpoint = context.Configuration.GetValue<string>("Notify:Channels:Webhook:Endpoint");
|
||||
if (string.IsNullOrWhiteSpace(endpoint))
|
||||
{
|
||||
issues.Add("Webhook channel enabled but endpoint not configured");
|
||||
}
|
||||
else
|
||||
{
|
||||
configuredChannels.Add("Webhook");
|
||||
}
|
||||
}
|
||||
|
||||
if (configuredChannels.Count == 0 && issues.Count == 0)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Info("No notification channels configured")
|
||||
.WithEvidence("Notify configuration", e =>
|
||||
{
|
||||
e.Add("ConfiguredChannels", "(none)");
|
||||
e.Add("Note", "Notifications are optional - configure channels to receive alerts");
|
||||
})
|
||||
.WithRemediation(r => r
|
||||
.AddStep(1, "Configure Email notifications",
|
||||
"# Add to appsettings.json:\n" +
|
||||
"\"Notify\": {\n" +
|
||||
" \"Channels\": {\n" +
|
||||
" \"Email\": {\n" +
|
||||
" \"Enabled\": true,\n" +
|
||||
" \"SmtpHost\": \"smtp.example.com\",\n" +
|
||||
" \"SmtpPort\": 587,\n" +
|
||||
" \"FromAddress\": \"alerts@example.com\"\n" +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
"}",
|
||||
CommandType.FileEdit)
|
||||
.AddStep(2, "Or run setup wizard",
|
||||
"stella setup --step notify",
|
||||
CommandType.Shell))
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build());
|
||||
}
|
||||
|
||||
if (issues.Count > 0 && configuredChannels.Count == 0)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Warn($"{issues.Count} channel configuration issue(s)")
|
||||
.WithEvidence("Notify configuration", e =>
|
||||
{
|
||||
e.Add("ConfiguredChannels", "(none - all have issues)");
|
||||
e.Add("Issues", string.Join("; ", issues));
|
||||
})
|
||||
.WithCauses(issues.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Review configuration", "Check Notify:Channels section for missing values")
|
||||
.AddStep(2, "Run setup wizard", "stella setup --step notify", CommandType.Shell))
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build());
|
||||
}
|
||||
|
||||
if (issues.Count > 0)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Warn($"{configuredChannels.Count} channel(s) configured, {issues.Count} issue(s)")
|
||||
.WithEvidence("Notify configuration", e =>
|
||||
{
|
||||
e.Add("ConfiguredChannels", string.Join(", ", configuredChannels));
|
||||
e.Add("Issues", string.Join("; ", issues));
|
||||
})
|
||||
.WithCauses(issues.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Review configuration", "Check Notify:Channels section for missing values"))
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build());
|
||||
}
|
||||
|
||||
return Task.FromResult(result
|
||||
.Pass($"{configuredChannels.Count} notification channel(s) configured")
|
||||
.WithEvidence("Notify configuration", e =>
|
||||
{
|
||||
e.Add("ConfiguredChannels", string.Join(", ", configuredChannels));
|
||||
e.Add("PrimaryChannel", configuredChannels[0]);
|
||||
})
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Notify.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates connectivity to configured notification channels.
|
||||
/// </summary>
|
||||
public sealed class NotifyChannelConnectivityCheck : IDoctorCheck
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.notify.channel.connectivity";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Notification Channel Connectivity";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Tests connectivity to configured notification channel endpoints";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["notify", "channel", "connectivity", "network"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context)
|
||||
{
|
||||
// Only run if at least one channel is configured
|
||||
var emailSection = context.Configuration.GetSection("Notify:Channels:Email");
|
||||
var slackSection = context.Configuration.GetSection("Notify:Channels:Slack");
|
||||
var teamsSection = context.Configuration.GetSection("Notify:Channels:Teams");
|
||||
var webhookSection = context.Configuration.GetSection("Notify:Channels:Webhook");
|
||||
|
||||
return emailSection.Exists() || slackSection.Exists() || teamsSection.Exists() || webhookSection.Exists();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.notify", DoctorCategory.Notify.ToString());
|
||||
|
||||
var connectivityResults = new List<(string Channel, bool Connected, string? Error)>();
|
||||
|
||||
// Test Email connectivity (SMTP)
|
||||
var emailEnabled = context.Configuration.GetValue<bool?>("Notify:Channels:Email:Enabled") ?? true;
|
||||
var smtpHost = context.Configuration.GetValue<string>("Notify:Channels:Email:SmtpHost");
|
||||
if (emailEnabled && !string.IsNullOrWhiteSpace(smtpHost))
|
||||
{
|
||||
var smtpPort = context.Configuration.GetValue<int?>("Notify:Channels:Email:SmtpPort") ?? 587;
|
||||
var emailResult = await TestSmtpConnectivityAsync(smtpHost, smtpPort, ct);
|
||||
connectivityResults.Add(("Email (SMTP)", emailResult.Success, emailResult.Error));
|
||||
}
|
||||
|
||||
// Test Slack connectivity
|
||||
var slackEnabled = context.Configuration.GetValue<bool?>("Notify:Channels:Slack:Enabled") ?? true;
|
||||
var slackWebhook = context.Configuration.GetValue<string>("Notify:Channels:Slack:WebhookUrl");
|
||||
if (slackEnabled && !string.IsNullOrWhiteSpace(slackWebhook))
|
||||
{
|
||||
var slackResult = await TestHttpEndpointAsync(slackWebhook, "Slack", ct);
|
||||
connectivityResults.Add(("Slack", slackResult.Success, slackResult.Error));
|
||||
}
|
||||
|
||||
// Test Teams connectivity
|
||||
var teamsEnabled = context.Configuration.GetValue<bool?>("Notify:Channels:Teams:Enabled") ?? true;
|
||||
var teamsWebhook = context.Configuration.GetValue<string>("Notify:Channels:Teams:WebhookUrl");
|
||||
if (teamsEnabled && !string.IsNullOrWhiteSpace(teamsWebhook))
|
||||
{
|
||||
var teamsResult = await TestHttpEndpointAsync(teamsWebhook, "Teams", ct);
|
||||
connectivityResults.Add(("Teams", teamsResult.Success, teamsResult.Error));
|
||||
}
|
||||
|
||||
// Test Webhook connectivity
|
||||
var webhookEnabled = context.Configuration.GetValue<bool?>("Notify:Channels:Webhook:Enabled") ?? true;
|
||||
var webhookEndpoint = context.Configuration.GetValue<string>("Notify:Channels:Webhook:Endpoint");
|
||||
if (webhookEnabled && !string.IsNullOrWhiteSpace(webhookEndpoint))
|
||||
{
|
||||
var webhookResult = await TestHttpEndpointAsync(webhookEndpoint, "Webhook", ct);
|
||||
connectivityResults.Add(("Webhook", webhookResult.Success, webhookResult.Error));
|
||||
}
|
||||
|
||||
if (connectivityResults.Count == 0)
|
||||
{
|
||||
return result
|
||||
.Skip("No notification channels configured to test")
|
||||
.Build();
|
||||
}
|
||||
|
||||
var failedChannels = connectivityResults.Where(r => !r.Connected).ToList();
|
||||
|
||||
if (failedChannels.Count > 0)
|
||||
{
|
||||
return result
|
||||
.Warn($"{failedChannels.Count} of {connectivityResults.Count} channel(s) unreachable")
|
||||
.WithEvidence("Connectivity results", e =>
|
||||
{
|
||||
foreach (var (channel, connected, error) in connectivityResults)
|
||||
{
|
||||
e.Add(channel, connected ? "Connected" : $"Failed: {error}");
|
||||
}
|
||||
})
|
||||
.WithCauses(failedChannels.Select(f => $"{f.Channel}: {f.Error}").ToArray())
|
||||
.WithRemediation(r =>
|
||||
{
|
||||
if (failedChannels.Any(f => f.Channel.Contains("SMTP")))
|
||||
{
|
||||
r.AddManualStep(1, "Check SMTP server", "Verify SMTP server is accessible and credentials are correct");
|
||||
}
|
||||
if (failedChannels.Any(f => f.Channel.Contains("Slack")))
|
||||
{
|
||||
r.AddManualStep(2, "Check Slack webhook", "Verify Slack webhook URL is valid and not expired");
|
||||
}
|
||||
if (failedChannels.Any(f => f.Channel.Contains("Teams")))
|
||||
{
|
||||
r.AddManualStep(3, "Check Teams webhook", "Verify Teams webhook URL is valid");
|
||||
}
|
||||
if (failedChannels.Any(f => f.Channel.Contains("Webhook")))
|
||||
{
|
||||
r.AddManualStep(4, "Check webhook endpoint", "Verify webhook endpoint is accessible from this network");
|
||||
}
|
||||
})
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build();
|
||||
}
|
||||
|
||||
return result
|
||||
.Pass($"All {connectivityResults.Count} channel(s) reachable")
|
||||
.WithEvidence("Connectivity results", e =>
|
||||
{
|
||||
foreach (var (channel, _, _) in connectivityResults)
|
||||
{
|
||||
e.Add(channel, "Connected");
|
||||
}
|
||||
})
|
||||
.Build();
|
||||
}
|
||||
|
||||
private static async Task<(bool Success, string? Error)> TestSmtpConnectivityAsync(
|
||||
string host,
|
||||
int port,
|
||||
CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = new System.Net.Sockets.TcpClient();
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
|
||||
await client.ConnectAsync(host, port, cts.Token);
|
||||
return (true, null);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return (false, "Connection timed out");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<(bool Success, string? Error)> TestHttpEndpointAsync(
|
||||
string url,
|
||||
string channelType,
|
||||
CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
||||
{
|
||||
return (false, "Invalid URL format");
|
||||
}
|
||||
|
||||
// Just test TCP connectivity to the host/port, don't send actual requests
|
||||
var port = uri.Port > 0 ? uri.Port : (uri.Scheme == "https" ? 443 : 80);
|
||||
|
||||
using var client = new System.Net.Sockets.TcpClient();
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
|
||||
await client.ConnectAsync(uri.Host, port, cts.Token);
|
||||
return (true, null);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return (false, "Connection timed out");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Notify.Checks;
|
||||
|
||||
/// <summary>
|
||||
/// Validates notification delivery capability by checking queue health and delivery configuration.
|
||||
/// </summary>
|
||||
public sealed class NotifyDeliveryTestCheck : IDoctorCheck
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string CheckId => "check.notify.delivery.test";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Notification Delivery Health";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => "Validates notification delivery queue and configuration health";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Tags => ["notify", "delivery", "queue", "health"];
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(2);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanRun(DoctorPluginContext context)
|
||||
{
|
||||
// Only run if notification system is configured
|
||||
var notifySection = context.Configuration.GetSection("Notify");
|
||||
return notifySection.Exists();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
|
||||
{
|
||||
var result = context.CreateResult(CheckId, "stellaops.doctor.notify", DoctorCategory.Notify.ToString());
|
||||
|
||||
var issues = new List<string>();
|
||||
var warnings = new List<string>();
|
||||
|
||||
// Check queue transport configuration
|
||||
var queueTransport = context.Configuration.GetValue<string>("Notify:Queue:Transport");
|
||||
var hasQueueConfig = !string.IsNullOrWhiteSpace(queueTransport);
|
||||
|
||||
if (!hasQueueConfig)
|
||||
{
|
||||
// Check for Redis or NATS configuration as fallback
|
||||
var redisConnection = context.Configuration.GetValue<string>("Notify:Queue:Redis:ConnectionString")
|
||||
?? context.Configuration.GetConnectionString("Redis");
|
||||
var natsConnection = context.Configuration.GetValue<string>("Notify:Queue:Nats:Url")
|
||||
?? context.Configuration.GetValue<string>("Nats:Url");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(redisConnection) && string.IsNullOrWhiteSpace(natsConnection))
|
||||
{
|
||||
warnings.Add("No queue transport configured - notifications may be processed in-memory only");
|
||||
}
|
||||
else
|
||||
{
|
||||
queueTransport = !string.IsNullOrWhiteSpace(redisConnection) ? "Redis" : "NATS";
|
||||
}
|
||||
}
|
||||
|
||||
// Check delivery retry configuration
|
||||
var maxRetries = context.Configuration.GetValue<int?>("Notify:Delivery:MaxRetries");
|
||||
if (maxRetries.HasValue && maxRetries.Value < 1)
|
||||
{
|
||||
issues.Add("MaxRetries is set to 0 or negative - failed deliveries will not be retried");
|
||||
}
|
||||
else if (maxRetries.HasValue && maxRetries.Value > 10)
|
||||
{
|
||||
warnings.Add($"MaxRetries ({maxRetries}) is high - consider reducing to avoid delivery delays");
|
||||
}
|
||||
|
||||
// Check throttle configuration
|
||||
var throttleEnabled = context.Configuration.GetValue<bool?>("Notify:Throttle:Enabled");
|
||||
var throttleWindow = context.Configuration.GetValue<string>("Notify:Throttle:Window");
|
||||
var throttleLimit = context.Configuration.GetValue<int?>("Notify:Throttle:Limit");
|
||||
|
||||
if (throttleEnabled == true)
|
||||
{
|
||||
if (throttleLimit.HasValue && throttleLimit.Value < 10)
|
||||
{
|
||||
warnings.Add($"Throttle limit ({throttleLimit}) is very low - may cause notification delays");
|
||||
}
|
||||
}
|
||||
|
||||
// Check digest configuration
|
||||
var digestEnabled = context.Configuration.GetValue<bool?>("Notify:Digest:Enabled");
|
||||
var digestInterval = context.Configuration.GetValue<string>("Notify:Digest:Interval");
|
||||
|
||||
// Check for default channel configuration
|
||||
var defaultChannel = context.Configuration.GetValue<string>("Notify:DefaultChannel");
|
||||
|
||||
if (issues.Count > 0)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Fail($"{issues.Count} delivery configuration issue(s)")
|
||||
.WithEvidence("Delivery configuration", e =>
|
||||
{
|
||||
e.Add("QueueTransport", queueTransport ?? "(not configured)");
|
||||
e.Add("MaxRetries", maxRetries?.ToString() ?? "(default)");
|
||||
e.Add("ThrottleEnabled", throttleEnabled?.ToString() ?? "(default)");
|
||||
e.Add("DigestEnabled", digestEnabled?.ToString() ?? "(default)");
|
||||
e.Add("DefaultChannel", defaultChannel ?? "(not set)");
|
||||
})
|
||||
.WithCauses(issues.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Review delivery settings", "Check Notify:Delivery section for invalid values")
|
||||
.AddStep(2, "Run setup wizard", "stella setup --step notify", CommandType.Shell))
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build());
|
||||
}
|
||||
|
||||
if (warnings.Count > 0)
|
||||
{
|
||||
return Task.FromResult(result
|
||||
.Warn($"{warnings.Count} delivery configuration recommendation(s)")
|
||||
.WithEvidence("Delivery configuration", e =>
|
||||
{
|
||||
e.Add("QueueTransport", queueTransport ?? "(in-memory)");
|
||||
e.Add("MaxRetries", maxRetries?.ToString() ?? "(default: 3)");
|
||||
e.Add("ThrottleEnabled", throttleEnabled?.ToString() ?? "(default: false)");
|
||||
e.Add("DigestEnabled", digestEnabled?.ToString() ?? "(default: false)");
|
||||
e.Add("DefaultChannel", defaultChannel ?? "(not set)");
|
||||
})
|
||||
.WithCauses(warnings.ToArray())
|
||||
.WithRemediation(r => r
|
||||
.AddManualStep(1, "Review recommendations", "Consider adjusting notification delivery settings")
|
||||
.AddStep(2, "Configure queue transport",
|
||||
"# Add to appsettings.json for Redis:\n" +
|
||||
"\"Notify\": {\n" +
|
||||
" \"Queue\": {\n" +
|
||||
" \"Transport\": \"Redis\",\n" +
|
||||
" \"Redis\": { \"ConnectionString\": \"localhost:6379\" }\n" +
|
||||
" }\n" +
|
||||
"}",
|
||||
CommandType.FileEdit))
|
||||
.WithVerification($"stella doctor --check {CheckId}")
|
||||
.Build());
|
||||
}
|
||||
|
||||
return Task.FromResult(result
|
||||
.Pass("Notification delivery configuration is healthy")
|
||||
.WithEvidence("Delivery configuration", e =>
|
||||
{
|
||||
e.Add("QueueTransport", queueTransport ?? "(in-memory)");
|
||||
e.Add("MaxRetries", maxRetries?.ToString() ?? "(default: 3)");
|
||||
e.Add("ThrottleEnabled", throttleEnabled?.ToString() ?? "(default: false)");
|
||||
e.Add("DigestEnabled", digestEnabled?.ToString() ?? "(default: false)");
|
||||
if (!string.IsNullOrWhiteSpace(defaultChannel))
|
||||
{
|
||||
e.Add("DefaultChannel", defaultChannel);
|
||||
}
|
||||
})
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.Notify.Checks;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Notify;
|
||||
|
||||
/// <summary>
|
||||
/// Plugin providing notification channel diagnostic checks including
|
||||
/// Email, Slack, Teams, and Webhook connectivity validation.
|
||||
/// </summary>
|
||||
public sealed class NotifyPlugin : IDoctorPlugin
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string PluginId => "stellaops.doctor.notify";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DisplayName => "Notifications";
|
||||
|
||||
/// <inheritdoc />
|
||||
public DoctorCategory Category => DoctorCategory.Notify;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Version Version => new(1, 0, 0);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Version MinEngineVersion => new(1, 0, 0);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsAvailable(IServiceProvider services) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<IDoctorCheck> GetChecks(DoctorPluginContext context) =>
|
||||
[
|
||||
new NotifyChannelConfigurationCheck(),
|
||||
new NotifyChannelConnectivityCheck(),
|
||||
new NotifyDeliveryTestCheck()
|
||||
];
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task InitializeAsync(DoctorPluginContext context, CancellationToken ct) => Task.CompletedTask;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<RootNamespace>StellaOps.Doctor.Plugins.Notify</RootNamespace>
|
||||
<Description>Doctor plugin for Notify channel diagnostics</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\StellaOps.Doctor\StellaOps.Doctor.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user