132 lines
4.6 KiB
C#
132 lines
4.6 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
using Npgsql;
|
|
using StellaOps.Doctor.Models;
|
|
using StellaOps.Doctor.Plugins;
|
|
|
|
namespace StellaOps.Doctor.Plugins.Database.Checks;
|
|
|
|
/// <summary>
|
|
/// Base class for database checks providing common functionality.
|
|
/// </summary>
|
|
public abstract class DatabaseCheckBase : IDoctorCheck
|
|
{
|
|
private const string DefaultConnectionStringKey = "ConnectionStrings:DefaultConnection";
|
|
private const string PluginId = "stellaops.doctor.database";
|
|
private const string CategoryName = "Database";
|
|
|
|
/// <inheritdoc />
|
|
public abstract string CheckId { get; }
|
|
|
|
/// <inheritdoc />
|
|
public abstract string Name { get; }
|
|
|
|
/// <inheritdoc />
|
|
public abstract string Description { get; }
|
|
|
|
/// <inheritdoc />
|
|
public virtual DoctorSeverity DefaultSeverity => DoctorSeverity.Fail;
|
|
|
|
/// <inheritdoc />
|
|
public abstract IReadOnlyList<string> Tags { get; }
|
|
|
|
/// <inheritdoc />
|
|
public virtual TimeSpan EstimatedDuration => TimeSpan.FromSeconds(2);
|
|
|
|
/// <inheritdoc />
|
|
public virtual bool CanRun(DoctorPluginContext context)
|
|
{
|
|
var connectionString = GetConnectionString(context);
|
|
return !string.IsNullOrEmpty(connectionString);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the runbook URL for the concrete check.
|
|
/// </summary>
|
|
protected abstract string RunbookUrl { get; }
|
|
|
|
/// <inheritdoc />
|
|
public async Task<DoctorCheckResult> 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=<host>;Port=5432;Database=<database>;Username=<user>;Password=<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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Executes the specific check logic.
|
|
/// </summary>
|
|
protected abstract Task<DoctorCheckResult> ExecuteCheckAsync(
|
|
DoctorPluginContext context,
|
|
string connectionString,
|
|
Builders.CheckResultBuilder result,
|
|
CancellationToken ct);
|
|
|
|
/// <summary>
|
|
/// Gets the database connection string from configuration.
|
|
/// </summary>
|
|
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];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new database connection.
|
|
/// </summary>
|
|
protected static async Task<NpgsqlConnection> CreateConnectionAsync(string connectionString, CancellationToken ct)
|
|
{
|
|
var connection = new NpgsqlConnection(connectionString);
|
|
await connection.OpenAsync(ct);
|
|
return connection;
|
|
}
|
|
}
|