Fix: Kafka replication factor hardcoded to 1 and No health check endpoints
This commit is contained in:
@@ -35,7 +35,7 @@ public class KafkaTopicProvisioner : IHostedService
|
|||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
NumPartitions = _options.NumPartitions,
|
NumPartitions = _options.NumPartitions,
|
||||||
ReplicationFactor = 1
|
ReplicationFactor = _options.ReplicationFactor
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ public class KafkaOptions
|
|||||||
public string BootstrapServers { get; set; } = null!;
|
public string BootstrapServers { get; set; } = null!;
|
||||||
public KafkaTopicOptions Topics { get; set; } = null!;
|
public KafkaTopicOptions Topics { get; set; } = null!;
|
||||||
public int NumPartitions { get; set; } = 6;
|
public int NumPartitions { get; set; } = 6;
|
||||||
|
public short ReplicationFactor { get; set; } = 3;
|
||||||
public int OutboxBatchSize { get; set; } = 100;
|
public int OutboxBatchSize { get; set; } = 100;
|
||||||
public int OutboxPollIntervalMs { get; set; } = 500;
|
public int OutboxPollIntervalMs { get; set; } = 500;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<HealthCheckResult> CheckHealthAsync(
|
||||||
|
HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var response = await _client.PingAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (response.IsValidResponse)
|
||||||
|
return HealthCheckResult.Healthy();
|
||||||
|
|
||||||
|
return HealthCheckResult.Unhealthy("Elasticsearch ping failed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<KafkaOptions> options) => _options = options.Value;
|
||||||
|
|
||||||
|
public async Task<HealthCheckResult> 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<string, object> { ["brokers"] = metadata.Brokers.Count };
|
||||||
|
return HealthCheckResult.Healthy(data: data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<RabbitMqOptions> options) => _options = options.Value;
|
||||||
|
|
||||||
|
public async Task<HealthCheckResult> 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<string, object> { ["endpoint"] = connection.Endpoint.ToString() };
|
||||||
|
return HealthCheckResult.Healthy(data: data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<HealthCheckResult> CheckHealthAsync(
|
||||||
|
HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var db = _redis.GetDatabase();
|
||||||
|
var latency = await db.PingAsync();
|
||||||
|
var data = new Dictionary<string, object> { ["ping_ms"] = latency.TotalMilliseconds };
|
||||||
|
return HealthCheckResult.Healthy(data: data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using Elastic.Clients.Elasticsearch;
|
using Elastic.Clients.Elasticsearch;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
using Prometheus;
|
using Prometheus;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
@@ -12,6 +13,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Microsoft.OpenApi;
|
||||||
|
|
||||||
Log.Logger = new LoggerConfiguration()
|
Log.Logger = new LoggerConfiguration()
|
||||||
.WriteTo.Console()
|
.WriteTo.Console()
|
||||||
@@ -197,6 +199,13 @@ try
|
|||||||
builder.Services.AddHostedService<GcsScoringService>();
|
builder.Services.AddHostedService<GcsScoringService>();
|
||||||
builder.Services.AddHostedService<SofaScoringService>();
|
builder.Services.AddHostedService<SofaScoringService>();
|
||||||
|
|
||||||
|
builder.Services.AddHealthChecks()
|
||||||
|
.AddDbContextCheck<AppDbContext>("postgresql", tags: new[] { "ready" })
|
||||||
|
.AddCheck<RedisHealthCheck>("redis", tags: new[] { "ready" })
|
||||||
|
.AddCheck<KafkaHealthCheck>("kafka", tags: new[] { "ready" })
|
||||||
|
.AddCheck<RabbitMqHealthCheck>("rabbitmq", tags: new[] { "ready" })
|
||||||
|
.AddCheck<ElasticsearchHealthCheck>("elasticsearch", tags: new[] { "ready" });
|
||||||
|
|
||||||
builder.Services.AddControllers()
|
builder.Services.AddControllers()
|
||||||
.AddJsonOptions(opts =>
|
.AddJsonOptions(opts =>
|
||||||
{
|
{
|
||||||
@@ -209,6 +218,31 @@ try
|
|||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen(options =>
|
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<string>()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
var xmlPath = Path.Combine(AppContext.BaseDirectory,
|
var xmlPath = Path.Combine(AppContext.BaseDirectory,
|
||||||
$"{Assembly.GetExecutingAssembly().GetName().Name}.xml");
|
$"{Assembly.GetExecutingAssembly().GetName().Name}.xml");
|
||||||
options.IncludeXmlComments(xmlPath);
|
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<CorrelationIdMiddleware>();
|
app.UseMiddleware<CorrelationIdMiddleware>();
|
||||||
app.UseMiddleware<FhirApiKeyOrJwtMiddleware>();
|
app.UseMiddleware<FhirApiKeyOrJwtMiddleware>();
|
||||||
app.UseMiddleware<ExceptionHandlerMiddleware>();
|
app.UseMiddleware<ExceptionHandlerMiddleware>();
|
||||||
@@ -269,12 +309,17 @@ try
|
|||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
app.MapHealthChecks("/health/live", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
|
||||||
if (app.Environment.IsDevelopment())
|
|
||||||
{
|
{
|
||||||
app.UseSwagger();
|
Predicate = _ => false,
|
||||||
app.UseSwaggerUI();
|
ResponseWriter = HealthCheckResponseWriter.WriteAsync
|
||||||
}
|
}).AllowAnonymous();
|
||||||
|
|
||||||
|
app.MapHealthChecks("/health/ready", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
|
||||||
|
{
|
||||||
|
Predicate = check => check.Tags.Contains("ready"),
|
||||||
|
ResponseWriter = HealthCheckResponseWriter.WriteAsync
|
||||||
|
}).AllowAnonymous();
|
||||||
|
|
||||||
app.MapMetrics("/metrics");
|
app.MapMetrics("/metrics");
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.4" />
|
||||||
<PackageReference Include="Minio" Version="6.0.3" />
|
<PackageReference Include="Minio" Version="6.0.3" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
|
||||||
<PackageReference Include="Parquet.Net" Version="4.24.0" />
|
<PackageReference Include="Parquet.Net" Version="4.24.0" />
|
||||||
|
|||||||
Reference in New Issue
Block a user