188 lines
7.7 KiB
C#
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
|
|
};
|
|
}
|
|
}
|