From 26e3226ae97989270c940800dce7b9ed7ac7a460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Tue, 23 Jan 2024 17:50:29 +0100 Subject: [PATCH] Project --- HealtRegistry/Containerfile | 15 +++++ HealtRegistry/HealtRegistry.csproj | 15 +++++ HealtRegistry/HealtRegistry.http | 18 ++++++ HealtRegistry/Models/HealthRequest.cs | 3 + HealtRegistry/Models/ServiceConfiguration.cs | 3 + HealtRegistry/Models/ServiceHealthData.cs | 7 +++ HealtRegistry/Models/ServiceHealthes.cs | 4 ++ HealtRegistry/Models/ServicesConfiguration.cs | 8 +++ HealtRegistry/Program.cs | 60 +++++++++++++++++++ HealtRegistry/Properties/launchSettings.json | 31 ++++++++++ HealtRegistry/Services/HealthService.cs | 34 +++++++++++ .../Services/HealthTimeoutService.cs | 51 ++++++++++++++++ HealtRegistry/Services/IHealthService.cs | 10 ++++ HealtRegistry/appsettings.Development.json | 14 +++++ HealtRegistry/appsettings.json | 9 +++ healthr.sln | 25 ++++++++ 16 files changed, 307 insertions(+) create mode 100644 HealtRegistry/Containerfile create mode 100644 HealtRegistry/HealtRegistry.csproj create mode 100644 HealtRegistry/HealtRegistry.http create mode 100644 HealtRegistry/Models/HealthRequest.cs create mode 100644 HealtRegistry/Models/ServiceConfiguration.cs create mode 100644 HealtRegistry/Models/ServiceHealthData.cs create mode 100644 HealtRegistry/Models/ServiceHealthes.cs create mode 100644 HealtRegistry/Models/ServicesConfiguration.cs create mode 100644 HealtRegistry/Program.cs create mode 100644 HealtRegistry/Properties/launchSettings.json create mode 100644 HealtRegistry/Services/HealthService.cs create mode 100644 HealtRegistry/Services/HealthTimeoutService.cs create mode 100644 HealtRegistry/Services/IHealthService.cs create mode 100644 HealtRegistry/appsettings.Development.json create mode 100644 HealtRegistry/appsettings.json create mode 100644 healthr.sln diff --git a/HealtRegistry/Containerfile b/HealtRegistry/Containerfile new file mode 100644 index 0000000..fb37304 --- /dev/null +++ b/HealtRegistry/Containerfile @@ -0,0 +1,15 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env +WORKDIR /build + +# Copy everything +COPY . ./ +# Restore as distinct layers +RUN dotnet restore +# Build and publish a release +RUN dotnet publish -c Release -o out + +# Build runtime image +FROM mcr.microsoft.com/dotnet/aspnet:8.0 +WORKDIR /app +COPY --from=build-env /build/out . +ENTRYPOINT ["dotnet", "HealtRegistry.dll"] diff --git a/HealtRegistry/HealtRegistry.csproj b/HealtRegistry/HealtRegistry.csproj new file mode 100644 index 0000000..fc1d56d --- /dev/null +++ b/HealtRegistry/HealtRegistry.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + true + + + + + + + + diff --git a/HealtRegistry/HealtRegistry.http b/HealtRegistry/HealtRegistry.http new file mode 100644 index 0000000..bf1f543 --- /dev/null +++ b/HealtRegistry/HealtRegistry.http @@ -0,0 +1,18 @@ +### Send health pdate for service 'test' +POST http://127.0.0.1:5081/health/test HTTP/1.0 + +### Set health of service 'test' to unhealthy +PUT http://127.0.0.1:5081/health/test HTTP/1.0 +Content-Type: application/json + +{ + "health": false +} + +### Set health of service 'test' to unhealthy +PUT https://healthr.adix.link/health/doorbell HTTP/1.0 +Content-Type: application/json + +{ + "health": false +} \ No newline at end of file diff --git a/HealtRegistry/Models/HealthRequest.cs b/HealtRegistry/Models/HealthRequest.cs new file mode 100644 index 0000000..8b4a29d --- /dev/null +++ b/HealtRegistry/Models/HealthRequest.cs @@ -0,0 +1,3 @@ +namespace HealtRegistry.Models; + +public record HealthRequest(bool? Health); \ No newline at end of file diff --git a/HealtRegistry/Models/ServiceConfiguration.cs b/HealtRegistry/Models/ServiceConfiguration.cs new file mode 100644 index 0000000..cbc89c9 --- /dev/null +++ b/HealtRegistry/Models/ServiceConfiguration.cs @@ -0,0 +1,3 @@ +namespace HealtRegistry.Models; + +public record ServiceConfiguration(bool DefaultValue, TimeSpan Timeout); \ No newline at end of file diff --git a/HealtRegistry/Models/ServiceHealthData.cs b/HealtRegistry/Models/ServiceHealthData.cs new file mode 100644 index 0000000..60c973e --- /dev/null +++ b/HealtRegistry/Models/ServiceHealthData.cs @@ -0,0 +1,7 @@ +namespace HealtRegistry.Models; + +public class ServiceHealthData +{ + public required bool Healthy { get; set; } + public required DateTimeOffset LastUpdated { get; set; } +} \ No newline at end of file diff --git a/HealtRegistry/Models/ServiceHealthes.cs b/HealtRegistry/Models/ServiceHealthes.cs new file mode 100644 index 0000000..f0c3af3 --- /dev/null +++ b/HealtRegistry/Models/ServiceHealthes.cs @@ -0,0 +1,4 @@ +namespace HealtRegistry.Models; + +public record ServiceHealth(bool Health); +public record ServiceHealthes(IReadOnlyDictionary Healthes); \ No newline at end of file diff --git a/HealtRegistry/Models/ServicesConfiguration.cs b/HealtRegistry/Models/ServicesConfiguration.cs new file mode 100644 index 0000000..70883e3 --- /dev/null +++ b/HealtRegistry/Models/ServicesConfiguration.cs @@ -0,0 +1,8 @@ +using System.ComponentModel.DataAnnotations; + +namespace HealtRegistry.Models; + +public class ServicesConfiguration +{ + [Required] public required Dictionary Services { get; init; } +} \ No newline at end of file diff --git a/HealtRegistry/Program.cs b/HealtRegistry/Program.cs new file mode 100644 index 0000000..cbed02d --- /dev/null +++ b/HealtRegistry/Program.cs @@ -0,0 +1,60 @@ +using HealtRegistry.Models; +using HealtRegistry.Services; +using Microsoft.AspNetCore.Mvc; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +builder.Services.AddSingleton(); + +builder.Services.AddHostedService(); + +builder.Services.AddOptions() + .Bind(builder.Configuration) + .ValidateDataAnnotations(); + +if (builder.Environment.IsProduction()) +{ + builder.Configuration.AddJsonFile("/config/appsettings.json", optional: true, reloadOnChange: true); +} + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.MapPost("/health/{service}", ([FromRoute] string service, IHealthService healthService) => +{ + healthService.SetServiceHealth(service, true); + return Results.Ok(); +}) +.WithOpenApi(); + +app.MapPut("/health/{service}", ([FromRoute] string service, [FromBody] HealthRequest requestBody, IHealthService healthService, [FromServices] ILoggerFactory loggerFactory) => +{ + if (requestBody.Health.HasValue) + { + healthService.SetServiceHealth(service, requestBody.Health.Value); + } + else + { + var logger = loggerFactory.CreateLogger("Health"); + logger.LogDebug("Health request body does not contains 'Health' property"); + } + return Results.Ok(); +}) +.WithOpenApi(); + +app.MapGet("/status", (IHealthService healthService) => +{ + return healthService.GetServiceHealthes(); +}) +.WithOpenApi(); + +app.Run(); diff --git a/HealtRegistry/Properties/launchSettings.json b/HealtRegistry/Properties/launchSettings.json new file mode 100644 index 0000000..633bb37 --- /dev/null +++ b/HealtRegistry/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:48758", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5081", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/HealtRegistry/Services/HealthService.cs b/HealtRegistry/Services/HealthService.cs new file mode 100644 index 0000000..752e429 --- /dev/null +++ b/HealtRegistry/Services/HealthService.cs @@ -0,0 +1,34 @@ +using HealtRegistry.Models; +using Microsoft.Extensions.Options; + +namespace HealtRegistry.Services; + +public class HealthService : IHealthService +{ + + public Dictionary ServiceHealthes { get; } = new(); + + public HealthService(IOptions servicesConfiguration) + { + foreach (var service in servicesConfiguration.Value.Services) + { + ServiceHealthes.Add(service.Key, new ServiceHealthData() + { + Healthy = service.Value.DefaultValue, + LastUpdated = DateTimeOffset.Now + }); + } + } + + public ServiceHealthes GetServiceHealthes() + { + var serviceHealthes = ServiceHealthes.ToDictionary(x => x.Key, x => new ServiceHealth(x.Value.Healthy)); + return new ServiceHealthes(serviceHealthes); + } + + public void SetServiceHealth(string serviceName, bool health) + { + ServiceHealthes[serviceName].Healthy = health; + ServiceHealthes[serviceName].LastUpdated = DateTimeOffset.Now; + } +} \ No newline at end of file diff --git a/HealtRegistry/Services/HealthTimeoutService.cs b/HealtRegistry/Services/HealthTimeoutService.cs new file mode 100644 index 0000000..f31a865 --- /dev/null +++ b/HealtRegistry/Services/HealthTimeoutService.cs @@ -0,0 +1,51 @@ + +using HealtRegistry.Models; +using Microsoft.Extensions.Options; + +namespace HealtRegistry.Services; + +public class HealthTimeoutService : BackgroundService +{ + private readonly IHealthService _healthService; + private readonly IOptionsMonitor _servicesConfiguration; + private readonly ILogger _logger; + + public HealthTimeoutService( + IHealthService healthService, + IOptionsMonitor servicesConfiguration, + ILogger logger + ) + { + _healthService = healthService; + _servicesConfiguration = servicesConfiguration; + _logger = logger; + } + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + var now = DateTimeOffset.Now; + foreach (var service in _healthService.ServiceHealthes) + { + if (!service.Value.Healthy) continue; + + var serviceConfiguration = _servicesConfiguration.CurrentValue.Services.TryGetValue(service.Key, out var configuration) + ? configuration + : null; + + if (serviceConfiguration is not null && + now - service.Value.LastUpdated > serviceConfiguration.Timeout) + { + _logger.LogInformation("Service {ServiceName} is timed out", service.Key); + _healthService.SetServiceHealth(service.Key, false); + } + } + + try + { + await Task.Delay(1000, stoppingToken); + } + catch { } + } + } +} \ No newline at end of file diff --git a/HealtRegistry/Services/IHealthService.cs b/HealtRegistry/Services/IHealthService.cs new file mode 100644 index 0000000..b455b1d --- /dev/null +++ b/HealtRegistry/Services/IHealthService.cs @@ -0,0 +1,10 @@ +using HealtRegistry.Models; + +namespace HealtRegistry.Services; + +public interface IHealthService +{ + Dictionary ServiceHealthes { get; } + ServiceHealthes GetServiceHealthes(); + void SetServiceHealth(string serviceName, bool health); +} \ No newline at end of file diff --git a/HealtRegistry/appsettings.Development.json b/HealtRegistry/appsettings.Development.json new file mode 100644 index 0000000..3b7204b --- /dev/null +++ b/HealtRegistry/appsettings.Development.json @@ -0,0 +1,14 @@ +{ + "Services": { + "test": { + "DefaultValue": false, + "Timeout": "00:00:30" + } + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/HealtRegistry/appsettings.json b/HealtRegistry/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/HealtRegistry/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/healthr.sln b/healthr.sln new file mode 100644 index 0000000..67a9e01 --- /dev/null +++ b/healthr.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HealtRegistry", "HealtRegistry\HealtRegistry.csproj", "{68A78D32-0CD2-4D54-84BB-4D49651965F7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {68A78D32-0CD2-4D54-84BB-4D49651965F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {68A78D32-0CD2-4D54-84BB-4D49651965F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {68A78D32-0CD2-4D54-84BB-4D49651965F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {68A78D32-0CD2-4D54-84BB-4D49651965F7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {4FF4B2FD-839C-49E5-B796-39DD55A59171} + EndGlobalSection +EndGlobal