using Microsoft.Extensions.Logging; using StellaOps.Platform.WebService.Contracts; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; namespace StellaOps.Platform.WebService.Services; /// /// Probes known crypto provider health endpoints and returns aggregated status. /// Providers are defined as a static seed list; unreachable providers return "unreachable" /// status (never a 500). /// public sealed class CryptoProviderHealthService { /// /// Well-known crypto provider definitions. These match the compose overlays /// in devops/compose/docker-compose.crypto-provider.*.yml. /// private static readonly IReadOnlyList KnownProviders = [ new CryptoProviderDefinition( Id: "smremote", Name: "SmRemote (SM2/SM3/SM4)", HealthEndpoint: "http://smremote.stella-ops.local/health", ComposeOverlay: "docker-compose.crypto-provider.smremote.yml", StartCommand: "docker compose -f docker-compose.stella-ops.yml -f docker-compose.crypto-provider.smremote.yml up -d smremote"), new CryptoProviderDefinition( Id: "cryptopro", Name: "CryptoPro CSP (GOST)", HealthEndpoint: "http://cryptopro-csp:8080/health", ComposeOverlay: "docker-compose.crypto-provider.cryptopro.yml", StartCommand: "CRYPTOPRO_ACCEPT_EULA=1 docker compose -f docker-compose.stella-ops.yml -f docker-compose.crypto-provider.cryptopro.yml up -d cryptopro-csp"), new CryptoProviderDefinition( Id: "crypto-sim", Name: "Crypto Simulator (dev/test)", HealthEndpoint: "http://sim-crypto:8080/keys", ComposeOverlay: "docker-compose.crypto-provider.crypto-sim.yml", StartCommand: "docker compose -f docker-compose.stella-ops.yml -f docker-compose.crypto-provider.crypto-sim.yml up -d sim-crypto"), ]; private readonly IHttpClientFactory httpClientFactory; private readonly TimeProvider timeProvider; private readonly ILogger logger; public CryptoProviderHealthService( IHttpClientFactory httpClientFactory, TimeProvider timeProvider, ILogger logger) { this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); this.timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// /// Returns the static list of known crypto provider definitions. /// public IReadOnlyList GetKnownProviders() => KnownProviders; /// /// Probes all known crypto providers concurrently and returns health status for each. /// public async Task ProbeAllAsync(CancellationToken cancellationToken) { var now = timeProvider.GetUtcNow(); var tasks = KnownProviders.Select(provider => ProbeProviderAsync(provider, now, cancellationToken)); var results = await Task.WhenAll(tasks).ConfigureAwait(false); return new CryptoProviderHealthResponse( Providers: results, CheckedAt: now); } private async Task ProbeProviderAsync( CryptoProviderDefinition definition, DateTimeOffset checkedAt, CancellationToken cancellationToken) { var client = httpClientFactory.CreateClient("CryptoProviderProbe"); try { var stopwatch = Stopwatch.StartNew(); using var request = new HttpRequestMessage(HttpMethod.Get, definition.HealthEndpoint); using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); stopwatch.Stop(); var latencyMs = Math.Round(stopwatch.Elapsed.TotalMilliseconds, 1); var status = response.IsSuccessStatusCode ? "running" : "degraded"; logger.LogDebug( "Crypto provider {ProviderId} probe: HTTP {StatusCode} in {LatencyMs}ms", definition.Id, (int)response.StatusCode, latencyMs); return new CryptoProviderHealthStatus( Id: definition.Id, Name: definition.Name, Status: status, LatencyMs: latencyMs, HealthEndpoint: definition.HealthEndpoint, ComposeOverlay: definition.ComposeOverlay, StartCommand: definition.StartCommand, CheckedAt: checkedAt); } catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or OperationCanceledException) { logger.LogDebug( ex, "Crypto provider {ProviderId} unreachable at {Endpoint}", definition.Id, definition.HealthEndpoint); return new CryptoProviderHealthStatus( Id: definition.Id, Name: definition.Name, Status: "unreachable", LatencyMs: null, HealthEndpoint: definition.HealthEndpoint, ComposeOverlay: definition.ComposeOverlay, StartCommand: definition.StartCommand, CheckedAt: checkedAt); } } }