diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e482a7a..ddc12df 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -156,3 +156,49 @@ jobs: Write-Warning "Warmup selhal: $($_.Exception.Message)" } } + + - name: Ověření zdraví aplikace + shell: powershell + env: + HEALTH_URL: ${{ vars.HEALTH_URL }} + run: | + $ErrorActionPreference = 'Stop' + if (-not $env:HEALTH_URL) { throw 'Chybí proměnná HEALTH_URL (vars.HEALTH_URL).' } + + $maxAttempts = 5 + $delaySec = 6 + $lastError = $null + $lastBody = $null + + for ($i = 1; $i -le $maxAttempts; $i++) { + try { + $resp = Invoke-WebRequest -Uri $env:HEALTH_URL -UseBasicParsing -TimeoutSec 10 + if ($resp.StatusCode -eq 200) { + Write-Host "Health check OK (pokus $i, status 200)." + Write-Host $resp.Content + exit 0 + } + $lastError = "HTTP $($resp.StatusCode)" + $lastBody = $resp.Content + } catch { + $webResp = $_.Exception.Response + if ($webResp) { + $lastError = "HTTP $([int]$webResp.StatusCode)" + try { + $reader = New-Object System.IO.StreamReader($webResp.GetResponseStream()) + $lastBody = $reader.ReadToEnd() + } catch { } + } else { + $lastError = $_.Exception.Message + } + } + + if ($i -lt $maxAttempts) { + Write-Host "Pokus ${i}/${maxAttempts}: $lastError. Zkousim znovu za $delaySec s..." + Start-Sleep -Seconds $delaySec + } + } + + Write-Host "::error::Health check selhal po $maxAttempts pokusech. Posledni chyba: $lastError" + if ($lastBody) { Write-Host "Telo posledni odpovedi:`n$lastBody" } + exit 1 diff --git a/OdectyMVC/DataLayer/RabbitMQProvider.cs b/OdectyMVC/DataLayer/RabbitMQProvider.cs index 2a4ab2b..d6d1902 100644 --- a/OdectyMVC/DataLayer/RabbitMQProvider.cs +++ b/OdectyMVC/DataLayer/RabbitMQProvider.cs @@ -11,6 +11,8 @@ public class RabbitMQProvider : IDisposable private readonly bool connected = false; private bool first = true; + public bool IsConnected => connected && connection?.IsOpen == true; + public RabbitMQProvider(IOptions options) { try diff --git a/OdectyMVC/HealthChecks/GaugeFileHealthCheck.cs b/OdectyMVC/HealthChecks/GaugeFileHealthCheck.cs new file mode 100644 index 0000000..955cf8d --- /dev/null +++ b/OdectyMVC/HealthChecks/GaugeFileHealthCheck.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace OdectyMVC.HealthChecks; + +public class GaugeFileHealthCheck : IHealthCheck +{ + private const string FileName = "GaugeList.json"; + + public Task CheckHealthAsync( + HealthCheckContext context, CancellationToken cancellationToken = default) + => Task.FromResult(File.Exists(FileName) + ? HealthCheckResult.Healthy($"{FileName} is present") + : HealthCheckResult.Unhealthy($"{FileName} is missing")); +} diff --git a/OdectyMVC/HealthChecks/RabbitMQHealthCheck.cs b/OdectyMVC/HealthChecks/RabbitMQHealthCheck.cs new file mode 100644 index 0000000..42b61e3 --- /dev/null +++ b/OdectyMVC/HealthChecks/RabbitMQHealthCheck.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; +using OdectyMVC.DataLayer; + +namespace OdectyMVC.HealthChecks; + +public class RabbitMQHealthCheck : IHealthCheck +{ + private readonly RabbitMQProvider provider; + + public RabbitMQHealthCheck(RabbitMQProvider provider) => this.provider = provider; + + public Task CheckHealthAsync( + HealthCheckContext context, CancellationToken cancellationToken = default) + => Task.FromResult(provider.IsConnected + ? HealthCheckResult.Healthy("RabbitMQ connection is open") + : HealthCheckResult.Unhealthy("RabbitMQ connection is not available")); +} diff --git a/OdectyMVC/OdectyMVC.csproj b/OdectyMVC/OdectyMVC.csproj index 8b2e3c0..7f47d22 100644 --- a/OdectyMVC/OdectyMVC.csproj +++ b/OdectyMVC/OdectyMVC.csproj @@ -7,13 +7,18 @@ + - - - + + + + + + + diff --git a/OdectyMVC/Program.cs b/OdectyMVC/Program.cs index b8cfa0a..e986fec 100644 --- a/OdectyMVC/Program.cs +++ b/OdectyMVC/Program.cs @@ -1,16 +1,55 @@ +using HealthChecks.UI.Client; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.Identity.Web; using Microsoft.OpenApi.Models; using OdectyMVC; using OdectyMVC.Application; using OdectyMVC.Contracts; using OdectyMVC.DataLayer; +using OdectyMVC.HealthChecks; using OdectyMVC.Middleware; using OdectyMVC.Options; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using Serilog; +using Serilog.Sinks.OpenTelemetry; using System.Security.Claims; var builder = WebApplication.CreateBuilder(args); + +const string serviceName = "OdectyMVC"; +var otlpEndpoint = builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"] ?? "http://localhost:4317"; + +builder.Host.UseSerilog((ctx, lc) => lc + .ReadFrom.Configuration(ctx.Configuration) + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.OpenTelemetry(opts => + { + opts.Endpoint = otlpEndpoint; + opts.Protocol = OtlpProtocol.Grpc; + opts.ResourceAttributes = new Dictionary + { + ["service.name"] = serviceName + }; + })); + +builder.Services.AddOpenTelemetry() + .ConfigureResource(r => r.AddService(serviceName)) + .WithTracing(t => t + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddSource("RabbitMQ.Client.Publisher", "RabbitMQ.Client.Subscriber") + .AddOtlpExporter()) + .WithMetrics(m => m + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation() + .AddOtlpExporter()); + // Add services to the container. builder.Services.AddControllersWithViews(); builder.Services.Configure(builder.Configuration.GetSection("RabbitMQSettings")); @@ -25,7 +64,10 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); -builder.Logging.AddEventLog(conf => conf.SourceName = "OdectyMVC"); + +builder.Services.AddHealthChecks() + .AddCheck("rabbitmq", tags: new[] { "ready" }) + .AddCheck("gauge-file", tags: new[] { "ready" }); #if !DEBUG builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) @@ -107,6 +149,18 @@ app.UseAuthorization(); #endif +app.MapHealthChecks("/health/live", new HealthCheckOptions +{ + Predicate = _ => false, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse +}).AllowAnonymous(); + +app.MapHealthChecks("/health/ready", new HealthCheckOptions +{ + Predicate = check => check.Tags.Contains("ready"), + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse +}).AllowAnonymous(); + app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{value?}");