Files
git.stella-ops.org/src/__Libraries/StellaOps.Doctor.Plugins.Sources/Checks/SourceConnectivityCheck.cs

188 lines
7.7 KiB
C#

// -----------------------------------------------------------------------------
// SourceConnectivityCheck.cs
// Sprint: SPRINT_20260114_SOURCES_SETUP
// Task: 12.1 - Sources Doctor Plugin
// Description: Individual source connectivity check with detailed remediation
// -----------------------------------------------------------------------------
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Concelier.Core.Sources;
using StellaOps.Doctor.Models;
using StellaOps.Doctor.Plugins;
using StellaOps.Doctor.Plugins.Builders;
using System.Globalization;
namespace StellaOps.Doctor.Plugins.Sources.Checks;
/// <summary>
/// Connectivity check for a single advisory data source.
/// Provides detailed error messages and remediation steps when connectivity fails.
/// </summary>
public sealed class SourceConnectivityCheck : IDoctorCheck
{
private readonly string _sourceId;
private readonly string _displayName;
/// <summary>
/// Creates a new source connectivity check for the specified source.
/// </summary>
/// <param name="sourceId">Source identifier.</param>
/// <param name="displayName">Human-readable source name.</param>
public SourceConnectivityCheck(string sourceId, string displayName)
{
_sourceId = sourceId;
_displayName = displayName;
}
/// <inheritdoc />
public string CheckId => $"check.sources.{_sourceId.ToLowerInvariant()}.connectivity";
/// <inheritdoc />
public string Name => $"{_displayName} Connectivity";
/// <inheritdoc />
public string Description => $"Verifies connectivity to {_displayName} advisory data source";
/// <inheritdoc />
public DoctorSeverity DefaultSeverity => DoctorSeverity.Warn;
/// <inheritdoc />
public IReadOnlyList<string> Tags => ["connectivity", "sources", _sourceId.ToLowerInvariant()];
/// <inheritdoc />
public TimeSpan EstimatedDuration => TimeSpan.FromSeconds(15);
/// <inheritdoc />
public bool CanRun(DoctorPluginContext context)
{
var registry = context.Services.GetService<ISourceRegistry>();
return registry?.GetSource(_sourceId) is not null;
}
/// <inheritdoc />
public async Task<DoctorCheckResult> RunAsync(DoctorPluginContext context, CancellationToken ct)
{
var result = context.CreateResult(CheckId, "stellaops.doctor.sources", DoctorCategory.Sources.ToString());
var registry = context.Services.GetService<ISourceRegistry>();
if (registry is null)
{
return result
.Skip("ISourceRegistry not available")
.Build();
}
var source = registry.GetSource(_sourceId);
if (source is null)
{
return result
.Skip($"Source {_sourceId} not found in registry")
.WithEvidence("Source lookup", e => e.Add("SourceId", _sourceId))
.Build();
}
// Perform connectivity check
var checkResult = await registry.CheckConnectivityAsync(_sourceId, ct);
if (checkResult.IsHealthy)
{
return result
.Pass($"{_displayName} is reachable (latency: {checkResult.Latency?.TotalMilliseconds:F0}ms)")
.WithEvidence("Connectivity check", e =>
{
e.Add("SourceId", _sourceId);
e.Add("DisplayName", _displayName);
e.Add("Status", checkResult.Status.ToString());
e.Add("LatencyMs", checkResult.Latency?.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture) ?? "N/A");
e.Add("CheckedAt", checkResult.CheckedAt.ToString("O", CultureInfo.InvariantCulture));
e.Add("Category", source.Category.ToString());
e.Add("HealthCheckEndpoint", source.HealthCheckEndpoint);
})
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
if (checkResult.Status == SourceConnectivityStatus.Degraded)
{
return result
.Warn($"{_displayName} is degraded: {checkResult.ErrorMessage}")
.WithEvidence("Connectivity check", e =>
{
e.Add("SourceId", _sourceId);
e.Add("DisplayName", _displayName);
e.Add("Status", checkResult.Status.ToString());
e.Add("ErrorCode", checkResult.ErrorCode ?? "UNKNOWN");
e.Add("ErrorMessage", checkResult.ErrorMessage ?? "No details available");
e.Add("CheckedAt", checkResult.CheckedAt.ToString("O", CultureInfo.InvariantCulture));
if (checkResult.HttpStatusCode.HasValue)
{
e.Add("HttpStatusCode", checkResult.HttpStatusCode.Value.ToString(CultureInfo.InvariantCulture));
}
})
.WithCauses(checkResult.PossibleReasons.ToArray())
.WithRemediation(r => BuildRemediation(r, checkResult))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
// Failed status
return result
.Fail($"{_displayName} connectivity failed: {checkResult.ErrorMessage}")
.WithEvidence("Connectivity check", e =>
{
e.Add("SourceId", _sourceId);
e.Add("DisplayName", _displayName);
e.Add("Status", checkResult.Status.ToString());
e.Add("ErrorCode", checkResult.ErrorCode ?? "UNKNOWN");
e.Add("ErrorMessage", checkResult.ErrorMessage ?? "No details available");
e.Add("CheckedAt", checkResult.CheckedAt.ToString("O", CultureInfo.InvariantCulture));
e.Add("HealthCheckEndpoint", source.HealthCheckEndpoint);
if (checkResult.HttpStatusCode.HasValue)
{
e.Add("HttpStatusCode", checkResult.HttpStatusCode.Value.ToString(CultureInfo.InvariantCulture));
}
if (checkResult.Latency.HasValue)
{
e.Add("LatencyMs", checkResult.Latency.Value.TotalMilliseconds.ToString("F0", CultureInfo.InvariantCulture));
}
})
.WithCauses(checkResult.PossibleReasons.ToArray())
.WithRemediation(r => BuildRemediation(r, checkResult))
.WithVerification($"stella doctor --check {CheckId}")
.Build();
}
private static void BuildRemediation(RemediationBuilder builder, SourceConnectivityResult checkResult)
{
foreach (var step in checkResult.RemediationSteps)
{
if (!string.IsNullOrEmpty(step.Command))
{
var commandType = MapCommandType(step.CommandType);
builder.AddStep(step.Order, step.Description, step.Command, commandType);
}
else
{
builder.AddManualStep(step.Order, step.Description, step.Description);
}
}
}
/// <summary>
/// Maps Concelier CommandType to Doctor CommandType.
/// </summary>
private static Doctor.Models.CommandType MapCommandType(Concelier.Core.Sources.CommandType sourceType)
{
return sourceType switch
{
Concelier.Core.Sources.CommandType.Bash => Doctor.Models.CommandType.Shell,
Concelier.Core.Sources.CommandType.PowerShell => Doctor.Models.CommandType.Shell,
Concelier.Core.Sources.CommandType.StellaCli => Doctor.Models.CommandType.Shell,
Concelier.Core.Sources.CommandType.Url => Doctor.Models.CommandType.Api,
Concelier.Core.Sources.CommandType.EnvVar => Doctor.Models.CommandType.Manual,
_ => Doctor.Models.CommandType.Shell
};
}
}