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);
}
}
}