From 0d0bba19e618a3d3f40f566fe244275de134f385 Mon Sep 17 00:00:00 2001 From: voltsrage Date: Sun, 21 Jun 2026 17:24:09 +0800 Subject: [PATCH] Fix: Kafka replication factor hardcoded to 1 and No health check endpoints --- .../KafkaTopicProvisioner.cs | 2 +- .../Configuration/KafkaOptions.cs | 1 + .../ElasticsearchHealthCheck.cs | 20 +++++++ .../HealthCheckResponseWriter.cs | 33 +++++++++++ .../Infrastructure/KafkaHealthCheck.cs | 25 ++++++++ .../Infrastructure/RabbitMqHealthCheck.cs | 26 +++++++++ .../Infrastructure/RedisHealthCheck.cs | 18 ++++++ VigilCareClinicalAPI/Program.cs | 57 +++++++++++++++++-- .../VigilCareClinicalAPI.csproj | 1 + 9 files changed, 176 insertions(+), 7 deletions(-) create mode 100644 VigilCareClinicalAPI/Infrastructure/ElasticsearchHealthCheck.cs create mode 100644 VigilCareClinicalAPI/Infrastructure/HealthCheckResponseWriter.cs create mode 100644 VigilCareClinicalAPI/Infrastructure/KafkaHealthCheck.cs create mode 100644 VigilCareClinicalAPI/Infrastructure/RabbitMqHealthCheck.cs create mode 100644 VigilCareClinicalAPI/Infrastructure/RedisHealthCheck.cs diff --git a/VigilCareClinicalAPI/BackgroundServices/KafkaTopicProvisioner.cs b/VigilCareClinicalAPI/BackgroundServices/KafkaTopicProvisioner.cs index 93e5885..9389d23 100644 --- a/VigilCareClinicalAPI/BackgroundServices/KafkaTopicProvisioner.cs +++ b/VigilCareClinicalAPI/BackgroundServices/KafkaTopicProvisioner.cs @@ -35,7 +35,7 @@ public class KafkaTopicProvisioner : IHostedService { Name = name, NumPartitions = _options.NumPartitions, - ReplicationFactor = 1 + ReplicationFactor = _options.ReplicationFactor }).ToList(); try diff --git a/VigilCareClinicalAPI/Configuration/KafkaOptions.cs b/VigilCareClinicalAPI/Configuration/KafkaOptions.cs index 53f481c..7865fdd 100644 --- a/VigilCareClinicalAPI/Configuration/KafkaOptions.cs +++ b/VigilCareClinicalAPI/Configuration/KafkaOptions.cs @@ -4,6 +4,7 @@ public class KafkaOptions public string BootstrapServers { get; set; } = null!; public KafkaTopicOptions Topics { get; set; } = null!; public int NumPartitions { get; set; } = 6; + public short ReplicationFactor { get; set; } = 3; public int OutboxBatchSize { get; set; } = 100; public int OutboxPollIntervalMs { get; set; } = 500; } diff --git a/VigilCareClinicalAPI/Infrastructure/ElasticsearchHealthCheck.cs b/VigilCareClinicalAPI/Infrastructure/ElasticsearchHealthCheck.cs new file mode 100644 index 0000000..d3dbe53 --- /dev/null +++ b/VigilCareClinicalAPI/Infrastructure/ElasticsearchHealthCheck.cs @@ -0,0 +1,20 @@ +using Elastic.Clients.Elasticsearch; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +public sealed class ElasticsearchHealthCheck : IHealthCheck +{ + private readonly ElasticsearchClient _client; + + public ElasticsearchHealthCheck(ElasticsearchClient client) => _client = client; + + public async Task CheckHealthAsync( + HealthCheckContext context, CancellationToken cancellationToken = default) + { + var response = await _client.PingAsync(cancellationToken); + + if (response.IsValidResponse) + return HealthCheckResult.Healthy(); + + return HealthCheckResult.Unhealthy("Elasticsearch ping failed."); + } +} diff --git a/VigilCareClinicalAPI/Infrastructure/HealthCheckResponseWriter.cs b/VigilCareClinicalAPI/Infrastructure/HealthCheckResponseWriter.cs new file mode 100644 index 0000000..e886aa8 --- /dev/null +++ b/VigilCareClinicalAPI/Infrastructure/HealthCheckResponseWriter.cs @@ -0,0 +1,33 @@ +using System.Text.Json; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +public static class HealthCheckResponseWriter +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }; + + public static async Task WriteAsync(HttpContext context, HealthReport report) + { + context.Response.ContentType = "application/json"; + + var response = new + { + status = report.Status.ToString(), + totalDurationMs = report.TotalDuration.TotalMilliseconds, + checks = report.Entries.Select(e => new + { + name = e.Key, + status = e.Value.Status.ToString(), + durationMs = e.Value.Duration.TotalMilliseconds, + description = e.Value.Description, + data = e.Value.Data.Count > 0 ? e.Value.Data : null, + exception = e.Value.Exception?.Message + }) + }; + + await context.Response.WriteAsync(JsonSerializer.Serialize(response, JsonOptions)); + } +} diff --git a/VigilCareClinicalAPI/Infrastructure/KafkaHealthCheck.cs b/VigilCareClinicalAPI/Infrastructure/KafkaHealthCheck.cs new file mode 100644 index 0000000..06bc569 --- /dev/null +++ b/VigilCareClinicalAPI/Infrastructure/KafkaHealthCheck.cs @@ -0,0 +1,25 @@ +using Confluent.Kafka; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Options; + +public sealed class KafkaHealthCheck : IHealthCheck +{ + private readonly KafkaOptions _options; + + public KafkaHealthCheck(IOptions options) => _options = options.Value; + + public async Task CheckHealthAsync( + HealthCheckContext context, CancellationToken cancellationToken = default) + { + using var admin = new AdminClientBuilder(new AdminClientConfig + { + BootstrapServers = _options.BootstrapServers + }).Build(); + + var metadata = await Task.Run( + () => admin.GetMetadata(TimeSpan.FromSeconds(5)), cancellationToken); + + var data = new Dictionary { ["brokers"] = metadata.Brokers.Count }; + return HealthCheckResult.Healthy(data: data); + } +} diff --git a/VigilCareClinicalAPI/Infrastructure/RabbitMqHealthCheck.cs b/VigilCareClinicalAPI/Infrastructure/RabbitMqHealthCheck.cs new file mode 100644 index 0000000..1440877 --- /dev/null +++ b/VigilCareClinicalAPI/Infrastructure/RabbitMqHealthCheck.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Options; +using RabbitMQ.Client; + +public sealed class RabbitMqHealthCheck : IHealthCheck +{ + private readonly RabbitMqOptions _options; + + public RabbitMqHealthCheck(IOptions options) => _options = options.Value; + + public async Task CheckHealthAsync( + HealthCheckContext context, CancellationToken cancellationToken = default) + { + var factory = new ConnectionFactory + { + HostName = _options.Host, + Port = _options.Port, + UserName = _options.Username, + Password = _options.Password + }; + + using var connection = await Task.Run(() => factory.CreateConnection(), cancellationToken); + var data = new Dictionary { ["endpoint"] = connection.Endpoint.ToString() }; + return HealthCheckResult.Healthy(data: data); + } +} diff --git a/VigilCareClinicalAPI/Infrastructure/RedisHealthCheck.cs b/VigilCareClinicalAPI/Infrastructure/RedisHealthCheck.cs new file mode 100644 index 0000000..9d3d698 --- /dev/null +++ b/VigilCareClinicalAPI/Infrastructure/RedisHealthCheck.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; +using StackExchange.Redis; + +public sealed class RedisHealthCheck : IHealthCheck +{ + private readonly IConnectionMultiplexer _redis; + + public RedisHealthCheck(IConnectionMultiplexer redis) => _redis = redis; + + public async Task CheckHealthAsync( + HealthCheckContext context, CancellationToken cancellationToken = default) + { + var db = _redis.GetDatabase(); + var latency = await db.PingAsync(); + var data = new Dictionary { ["ping_ms"] = latency.TotalMilliseconds }; + return HealthCheckResult.Healthy(data: data); + } +} diff --git a/VigilCareClinicalAPI/Program.cs b/VigilCareClinicalAPI/Program.cs index b23da06..7dc02e4 100644 --- a/VigilCareClinicalAPI/Program.cs +++ b/VigilCareClinicalAPI/Program.cs @@ -1,5 +1,6 @@ using Elastic.Clients.Elasticsearch; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Diagnostics.HealthChecks; using Prometheus; using Serilog; using StackExchange.Redis; @@ -12,6 +13,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using Microsoft.AspNetCore.Authorization; using System.Text; +using Microsoft.OpenApi; Log.Logger = new LoggerConfiguration() .WriteTo.Console() @@ -197,6 +199,13 @@ try builder.Services.AddHostedService(); builder.Services.AddHostedService(); + builder.Services.AddHealthChecks() + .AddDbContextCheck("postgresql", tags: new[] { "ready" }) + .AddCheck("redis", tags: new[] { "ready" }) + .AddCheck("kafka", tags: new[] { "ready" }) + .AddCheck("rabbitmq", tags: new[] { "ready" }) + .AddCheck("elasticsearch", tags: new[] { "ready" }); + builder.Services.AddControllers() .AddJsonOptions(opts => { @@ -209,6 +218,31 @@ try builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { + options.SwaggerDoc("v1", new OpenApiInfo + { + Title = "VigilCare Clinical API", + Version = "v1", + Description = "Clinical monitoring and alerting platform API" + }); + + options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Name = "Authorization", + Type = SecuritySchemeType.Http, + Scheme = "bearer", + BearerFormat = "JWT", + In = ParameterLocation.Header, + Description = "Enter your JWT token" + }); + + options.AddSecurityRequirement(document => new OpenApiSecurityRequirement + { + { + new OpenApiSecuritySchemeReference("Bearer", document), + new List() + } + }); + var xmlPath = Path.Combine(AppContext.BaseDirectory, $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"); options.IncludeXmlComments(xmlPath); @@ -260,6 +294,12 @@ try }); } + app.UseSwagger(); + app.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/swagger/v1/swagger.json", "VigilCare Clinical API v1"); + }); + app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(); @@ -269,14 +309,19 @@ try app.UseAuthentication(); app.UseAuthorization(); - // Configure the HTTP request pipeline. - if (app.Environment.IsDevelopment()) + app.MapHealthChecks("/health/live", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions { - app.UseSwagger(); - app.UseSwaggerUI(); - } + Predicate = _ => false, + ResponseWriter = HealthCheckResponseWriter.WriteAsync + }).AllowAnonymous(); - app.MapMetrics("/metrics"); + app.MapHealthChecks("/health/ready", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions + { + Predicate = check => check.Tags.Contains("ready"), + ResponseWriter = HealthCheckResponseWriter.WriteAsync + }).AllowAnonymous(); + + app.MapMetrics("/metrics"); app.MapControllers(); app.Run(); diff --git a/VigilCareClinicalAPI/VigilCareClinicalAPI.csproj b/VigilCareClinicalAPI/VigilCareClinicalAPI.csproj index 6645b64..5b532a9 100644 --- a/VigilCareClinicalAPI/VigilCareClinicalAPI.csproj +++ b/VigilCareClinicalAPI/VigilCareClinicalAPI.csproj @@ -24,6 +24,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all +