using Microsoft.Extensions.Configuration; using Npgsql; using StellaOps.Doctor.Models; using StellaOps.Doctor.Plugins; namespace StellaOps.Doctor.Plugins.Database.Checks; /// /// Base class for database checks providing common functionality. /// public abstract class DatabaseCheckBase : IDoctorCheck { private const string DefaultConnectionStringKey = "ConnectionStrings:DefaultConnection"; private const string PluginId = "stellaops.doctor.database"; private const string CategoryName = "Database"; /// public abstract string CheckId { get; } /// public abstract string Name { get; } /// public abstract string Description { get; } /// public virtual DoctorSeverity DefaultSeverity => DoctorSeverity.Fail; /// public abstract IReadOnlyList Tags { get; } /// public virtual TimeSpan EstimatedDuration => TimeSpan.FromSeconds(2); /// public virtual bool CanRun(DoctorPluginContext context) { var connectionString = GetConnectionString(context); return !string.IsNullOrEmpty(connectionString); } /// /// Gets the runbook URL for the concrete check. /// protected abstract string RunbookUrl { get; } /// public async Task RunAsync(DoctorPluginContext context, CancellationToken ct) { var result = context.CreateResult(CheckId, PluginId, CategoryName); var connectionString = GetConnectionString(context); if (string.IsNullOrEmpty(connectionString)) { return result .Skip("No database connection string configured") .WithEvidence("Configuration", e => e .Add("ConnectionStringKey", DefaultConnectionStringKey) .Add("Configured", "false")) .Build(); } try { return await ExecuteCheckAsync(context, connectionString, result, ct); } catch (NpgsqlException ex) { return result .Fail($"Database error: {ex.Message}") .WithEvidence("Error details", e => e .Add("ExceptionType", ex.GetType().Name) .Add("SqlState", ex.SqlState ?? "(none)") .Add("Message", ex.Message)) .WithCauses( "Database server unavailable", "Authentication failed", "Network connectivity issue") .WithRemediation(r => r .AddShellStep(1, "Test connection", "psql \"Host=;Port=5432;Database=;Username=;Password=\" -c \"SELECT 1\"") .AddManualStep(2, "Check configuration", "Verify ConnectionStrings__DefaultConnection or Doctor__Plugins__Database__ConnectionString points to the intended PostgreSQL instance") .WithRunbookUrl(RunbookUrl)) .WithVerification($"stella doctor --check {CheckId}") .Build(); } catch (Exception ex) { return result .Fail($"Unexpected error: {ex.Message}") .WithEvidence("Error details", e => e .Add("ExceptionType", ex.GetType().Name) .Add("Message", ex.Message)) .Build(); } } /// /// Executes the specific check logic. /// protected abstract Task ExecuteCheckAsync( DoctorPluginContext context, string connectionString, Builders.CheckResultBuilder result, CancellationToken ct); /// /// Gets the database connection string from configuration. /// protected static string? GetConnectionString(DoctorPluginContext context) { // Try plugin-specific connection string first var pluginConnectionString = context.PluginConfig["ConnectionString"]; if (!string.IsNullOrEmpty(pluginConnectionString)) { return pluginConnectionString; } // Fall back to default connection string return context.Configuration[DefaultConnectionStringKey]; } /// /// Creates a new database connection. /// protected static async Task CreateConnectionAsync(string connectionString, CancellationToken ct) { var connection = new NpgsqlConnection(connectionString); await connection.OpenAsync(ct); return connection; } }