feat(health): add custom DB, Qdrant, and Neo4j health check services and secure Qdrant in staging

This commit is contained in:
2026-06-14 15:15:58 +02:00
parent d2410e9793
commit a738a28eb4
7 changed files with 119 additions and 1 deletions
+1
View File
@@ -30,6 +30,7 @@ services:
- ASPNETCORE_ENVIRONMENT=Staging
- ConnectionStrings__PostgresConnection=Host=db;Database=${POSTGRES_DB:-nexus_stage_db};Username=${POSTGRES_USER:-nexus_user_stage};Password=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
- ConnectionStrings__QdrantConnection=http://qdrant:6334
- Qdrant__ApiKey=${QDRANT_API_KEY:-}
- ConnectionStrings__Neo4jConnection=bolt://neo4j:7687
- Neo4j__Username=${NEO4J_USERNAME:-neo4j}
- Neo4j__Password=${NEO4J_PASSWORD:?NEO4J_PASSWORD is required}
+6
View File
@@ -37,6 +37,12 @@ if grep -q "CHANGE_ME_TO_SECURE_ADMIN_PASSWORD" "$ENV_FILE"; then
sed -i "s/NEXUS_ADMIN_PASSWORD=CHANGE_ME_TO_SECURE_ADMIN_PASSWORD/NEXUS_ADMIN_PASSWORD=$ADMIN_PASS/g" "$ENV_FILE"
fi
if grep -q "^QDRANT_API_KEY=$" "$ENV_FILE" || grep -q "^QDRANT_API_KEY=[[:space:]]*$" "$ENV_FILE"; then
echo "🔐 Generating secure random Qdrant API key in $ENV_FILE..."
QD_KEY=$(openssl rand -hex 16)
sed -i "s/^QDRANT_API_KEY=.*/QDRANT_API_KEY=$QD_KEY/g" "$ENV_FILE"
fi
# Load staging variables for local execution context (needed for ports/migrations)
# Clean up carriage returns just in case
POSTGRES_USER=$(grep "^POSTGRES_USER=" "$ENV_FILE" | cut -d'=' -f2- | tr -d '\r')
@@ -55,7 +55,15 @@ public static class DependencyInjection
// Qdrant Client registration
var qdrantUrl = configuration.GetConnectionString("QdrantConnection") ?? "http://localhost:6334";
services.AddSingleton<QdrantClient>(sp => new QdrantClient(new Uri(qdrantUrl)));
var qdrantApiKey = configuration["Qdrant:ApiKey"];
services.AddSingleton<QdrantClient>(sp =>
{
if (!string.IsNullOrEmpty(qdrantApiKey))
{
return new QdrantClient(new Uri(qdrantUrl), apiKey: qdrantApiKey);
}
return new QdrantClient(new Uri(qdrantUrl));
});
// Neo4j Driver registration (supports optional authentication)
var neo4jUrl = configuration.GetConnectionString("Neo4jConnection") ?? "bolt://localhost:7687";
+5
View File
@@ -91,6 +91,10 @@ builder.Services.AddCascadingAuthenticationState();
builder.Services.AddApplication();
builder.Services.AddInfrastructure(builder.Configuration);
builder.Services.AddHealthChecks()
.AddCheck<NexusReader.Web.Services.DatabaseHealthCheck>("Database")
.AddCheck<NexusReader.Web.Services.QdrantHealthCheck>("Qdrant")
.AddCheck<NexusReader.Web.Services.Neo4jHealthCheck>("Neo4j");
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(
NexusReader.Application.DependencyInjection.Assembly,
@@ -295,6 +299,7 @@ if (!allowRegistration || !allowPasswordReset)
}
app.MapStaticAssets();
app.MapHealthChecks("/health");
app.MapHub<NexusReader.Infrastructure.RealTime.SyncHub>("/synchub");
// API endpoint for WASM client to fetch EPUB content
@@ -0,0 +1,35 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using NexusReader.Data.Persistence;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace NexusReader.Web.Services;
public class DatabaseHealthCheck : IHealthCheck
{
private readonly AppDbContext _dbContext;
public DatabaseHealthCheck(AppDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
var canConnect = await _dbContext.Database.CanConnectAsync(cancellationToken);
if (canConnect)
{
return HealthCheckResult.Healthy("Database is accessible.");
}
return HealthCheckResult.Unhealthy("Cannot connect to the database.");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("Database health check failed with exception.", ex);
}
}
}
@@ -0,0 +1,31 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Neo4j.Driver;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace NexusReader.Web.Services;
public class Neo4jHealthCheck : IHealthCheck
{
private readonly IDriver _driver;
public Neo4jHealthCheck(IDriver driver)
{
_driver = driver;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
await _driver.VerifyConnectivityAsync();
return HealthCheckResult.Healthy("Neo4j database is accessible.");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("Neo4j database connectivity check failed.", ex);
}
}
}
@@ -0,0 +1,32 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Qdrant.Client;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace NexusReader.Web.Services;
public class QdrantHealthCheck : IHealthCheck
{
private readonly QdrantClient _qdrantClient;
public QdrantHealthCheck(QdrantClient qdrantClient)
{
_qdrantClient = qdrantClient;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
// Simple check: query collection existence to verify connection is alive
_ = await _qdrantClient.CollectionExistsAsync("knowledge_units", cancellationToken);
return HealthCheckResult.Healthy("Qdrant database is accessible.");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("Qdrant database health check failed.", ex);
}
}
}