321 lines
14 KiB
C#
321 lines
14 KiB
C#
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
|
using System.Diagnostics;
|
|
using System.Text.Json;
|
|
|
|
namespace QRRapidoApp.Services.HealthChecks
|
|
{
|
|
public class ExternalServicesHealthCheck : IHealthCheck
|
|
{
|
|
private readonly IConfiguration _configuration;
|
|
private readonly ILogger<ExternalServicesHealthCheck> _logger;
|
|
private readonly HttpClient _httpClient;
|
|
|
|
private readonly int _timeoutSeconds;
|
|
private readonly bool _testStripeConnection;
|
|
private readonly bool _testGoogleAuth;
|
|
private readonly bool _testMicrosoftAuth;
|
|
|
|
public ExternalServicesHealthCheck(
|
|
IConfiguration configuration,
|
|
ILogger<ExternalServicesHealthCheck> logger,
|
|
IHttpClientFactory httpClientFactory)
|
|
{
|
|
_configuration = configuration;
|
|
_logger = logger;
|
|
_httpClient = httpClientFactory.CreateClient();
|
|
|
|
_timeoutSeconds = configuration.GetValue<int>("HealthChecks:ExternalServices:TimeoutSeconds", 10);
|
|
_testStripeConnection = configuration.GetValue<bool>("HealthChecks:ExternalServices:TestStripeConnection", true);
|
|
_testGoogleAuth = configuration.GetValue<bool>("HealthChecks:ExternalServices:TestGoogleAuth", false);
|
|
_testMicrosoftAuth = configuration.GetValue<bool>("HealthChecks:ExternalServices:TestMicrosoftAuth", false);
|
|
}
|
|
|
|
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
|
{
|
|
var stopwatch = Stopwatch.StartNew();
|
|
var data = new Dictionary<string, object>();
|
|
var services = new List<object>();
|
|
var issues = new List<string>();
|
|
var warnings = new List<string>();
|
|
|
|
try
|
|
{
|
|
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(_timeoutSeconds));
|
|
using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
|
|
|
|
// Test Stripe connection
|
|
if (_testStripeConnection)
|
|
{
|
|
var stripeResult = await TestStripeConnectionAsync(combinedCts.Token);
|
|
services.Add(stripeResult);
|
|
|
|
if (stripeResult.GetProperty("status").GetString() == "error")
|
|
{
|
|
issues.Add($"Stripe: {stripeResult.GetProperty("error").GetString()}");
|
|
}
|
|
else if (stripeResult.GetProperty("status").GetString() == "warning")
|
|
{
|
|
warnings.Add($"Stripe: slow response ({stripeResult.GetProperty("latencyMs").GetInt32()}ms)");
|
|
}
|
|
}
|
|
|
|
// Test Google OAuth endpoints
|
|
if (_testGoogleAuth)
|
|
{
|
|
var googleResult = await TestGoogleAuthAsync(combinedCts.Token);
|
|
services.Add(googleResult);
|
|
|
|
if (googleResult.GetProperty("status").GetString() == "error")
|
|
{
|
|
warnings.Add($"Google Auth: {googleResult.GetProperty("error").GetString()}");
|
|
}
|
|
}
|
|
|
|
// Test Microsoft OAuth endpoints
|
|
if (_testMicrosoftAuth)
|
|
{
|
|
var microsoftResult = await TestMicrosoftAuthAsync(combinedCts.Token);
|
|
services.Add(microsoftResult);
|
|
|
|
if (microsoftResult.GetProperty("status").GetString() == "error")
|
|
{
|
|
warnings.Add($"Microsoft Auth: {microsoftResult.GetProperty("error").GetString()}");
|
|
}
|
|
}
|
|
|
|
stopwatch.Stop();
|
|
|
|
data["services"] = services;
|
|
data["totalCheckTimeMs"] = stopwatch.ElapsedMilliseconds;
|
|
data["status"] = DetermineOverallStatus(issues.Count, warnings.Count);
|
|
|
|
// Return appropriate health status
|
|
if (issues.Any())
|
|
{
|
|
return HealthCheckResult.Unhealthy($"External service issues: {string.Join(", ", issues)}", data: data);
|
|
}
|
|
|
|
if (warnings.Any())
|
|
{
|
|
return HealthCheckResult.Degraded($"External service warnings: {string.Join(", ", warnings)}", data: data);
|
|
}
|
|
|
|
return HealthCheckResult.Healthy($"All external services healthy ({services.Count} services checked)", data: data);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
return HealthCheckResult.Unhealthy($"External services health check timed out after {_timeoutSeconds} seconds", data: data);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "External services health check failed");
|
|
return HealthCheckResult.Unhealthy($"External services health check failed: {ex.Message}", data: data);
|
|
}
|
|
}
|
|
|
|
private async Task<JsonElement> TestStripeConnectionAsync(CancellationToken cancellationToken)
|
|
{
|
|
var serviceStopwatch = Stopwatch.StartNew();
|
|
|
|
try
|
|
{
|
|
var secretKey = _configuration["Stripe:SecretKey"];
|
|
if (string.IsNullOrEmpty(secretKey) || secretKey.StartsWith("sk_test_xxxxx"))
|
|
{
|
|
return JsonDocument.Parse(JsonSerializer.Serialize(new
|
|
{
|
|
service = "stripe",
|
|
status = "warning",
|
|
error = "Stripe not configured",
|
|
latencyMs = serviceStopwatch.ElapsedMilliseconds
|
|
})).RootElement;
|
|
}
|
|
|
|
// Test Stripe API connectivity by hitting a simple endpoint
|
|
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.stripe.com/v1/account");
|
|
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", secretKey);
|
|
|
|
var response = await _httpClient.SendAsync(request, cancellationToken);
|
|
serviceStopwatch.Stop();
|
|
|
|
var latencyMs = serviceStopwatch.ElapsedMilliseconds;
|
|
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
var status = latencyMs > 2000 ? "warning" : "ok";
|
|
return JsonDocument.Parse(JsonSerializer.Serialize(new
|
|
{
|
|
service = "stripe",
|
|
status = status,
|
|
statusCode = (int)response.StatusCode,
|
|
latencyMs = latencyMs,
|
|
lastChecked = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")
|
|
})).RootElement;
|
|
}
|
|
else
|
|
{
|
|
return JsonDocument.Parse(JsonSerializer.Serialize(new
|
|
{
|
|
service = "stripe",
|
|
status = "error",
|
|
statusCode = (int)response.StatusCode,
|
|
error = $"HTTP {response.StatusCode}",
|
|
latencyMs = latencyMs
|
|
})).RootElement;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
serviceStopwatch.Stop();
|
|
_logger.LogWarning(ex, "Failed to test Stripe connection");
|
|
|
|
return JsonDocument.Parse(JsonSerializer.Serialize(new
|
|
{
|
|
service = "stripe",
|
|
status = "error",
|
|
error = ex.Message,
|
|
latencyMs = serviceStopwatch.ElapsedMilliseconds
|
|
})).RootElement;
|
|
}
|
|
}
|
|
|
|
private async Task<JsonElement> TestGoogleAuthAsync(CancellationToken cancellationToken)
|
|
{
|
|
var serviceStopwatch = Stopwatch.StartNew();
|
|
|
|
try
|
|
{
|
|
var clientId = _configuration["Authentication:Google:ClientId"];
|
|
if (string.IsNullOrEmpty(clientId) || clientId == "your-google-client-id")
|
|
{
|
|
return JsonDocument.Parse(JsonSerializer.Serialize(new
|
|
{
|
|
service = "google_auth",
|
|
status = "warning",
|
|
error = "Google Auth not configured",
|
|
latencyMs = serviceStopwatch.ElapsedMilliseconds
|
|
})).RootElement;
|
|
}
|
|
|
|
// Test Google's OAuth discovery document
|
|
var response = await _httpClient.GetAsync("https://accounts.google.com/.well-known/openid_configuration", cancellationToken);
|
|
serviceStopwatch.Stop();
|
|
|
|
var latencyMs = serviceStopwatch.ElapsedMilliseconds;
|
|
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
return JsonDocument.Parse(JsonSerializer.Serialize(new
|
|
{
|
|
service = "google_auth",
|
|
status = "ok",
|
|
statusCode = (int)response.StatusCode,
|
|
latencyMs = latencyMs,
|
|
lastChecked = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")
|
|
})).RootElement;
|
|
}
|
|
else
|
|
{
|
|
return JsonDocument.Parse(JsonSerializer.Serialize(new
|
|
{
|
|
service = "google_auth",
|
|
status = "error",
|
|
statusCode = (int)response.StatusCode,
|
|
error = $"HTTP {response.StatusCode}",
|
|
latencyMs = latencyMs
|
|
})).RootElement;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
serviceStopwatch.Stop();
|
|
_logger.LogWarning(ex, "Failed to test Google Auth connection");
|
|
|
|
return JsonDocument.Parse(JsonSerializer.Serialize(new
|
|
{
|
|
service = "google_auth",
|
|
status = "error",
|
|
error = ex.Message,
|
|
latencyMs = serviceStopwatch.ElapsedMilliseconds
|
|
})).RootElement;
|
|
}
|
|
}
|
|
|
|
private async Task<JsonElement> TestMicrosoftAuthAsync(CancellationToken cancellationToken)
|
|
{
|
|
var serviceStopwatch = Stopwatch.StartNew();
|
|
|
|
try
|
|
{
|
|
var clientId = _configuration["Authentication:Microsoft:ClientId"];
|
|
if (string.IsNullOrEmpty(clientId) || clientId == "your-microsoft-client-id")
|
|
{
|
|
return JsonDocument.Parse(JsonSerializer.Serialize(new
|
|
{
|
|
service = "microsoft_auth",
|
|
status = "warning",
|
|
error = "Microsoft Auth not configured",
|
|
latencyMs = serviceStopwatch.ElapsedMilliseconds
|
|
})).RootElement;
|
|
}
|
|
|
|
// Test Microsoft's OAuth discovery document
|
|
var response = await _httpClient.GetAsync("https://login.microsoftonline.com/common/v2.0/.well-known/openid_configuration", cancellationToken);
|
|
serviceStopwatch.Stop();
|
|
|
|
var latencyMs = serviceStopwatch.ElapsedMilliseconds;
|
|
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
return JsonDocument.Parse(JsonSerializer.Serialize(new
|
|
{
|
|
service = "microsoft_auth",
|
|
status = "ok",
|
|
statusCode = (int)response.StatusCode,
|
|
latencyMs = latencyMs,
|
|
lastChecked = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")
|
|
})).RootElement;
|
|
}
|
|
else
|
|
{
|
|
return JsonDocument.Parse(JsonSerializer.Serialize(new
|
|
{
|
|
service = "microsoft_auth",
|
|
status = "error",
|
|
statusCode = (int)response.StatusCode,
|
|
error = $"HTTP {response.StatusCode}",
|
|
latencyMs = latencyMs
|
|
})).RootElement;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
serviceStopwatch.Stop();
|
|
_logger.LogWarning(ex, "Failed to test Microsoft Auth connection");
|
|
|
|
return JsonDocument.Parse(JsonSerializer.Serialize(new
|
|
{
|
|
service = "microsoft_auth",
|
|
status = "error",
|
|
error = ex.Message,
|
|
latencyMs = serviceStopwatch.ElapsedMilliseconds
|
|
})).RootElement;
|
|
}
|
|
}
|
|
|
|
private string DetermineOverallStatus(int issueCount, int warningCount)
|
|
{
|
|
if (issueCount > 0)
|
|
{
|
|
return "error";
|
|
}
|
|
|
|
if (warningCount > 0)
|
|
{
|
|
return "warning";
|
|
}
|
|
|
|
return "ok";
|
|
}
|
|
}
|
|
} |