add health check
This commit is contained in:
		
							parent
							
								
									de25b69a4b
								
							
						
					
					
						commit
						053bfb0d92
					
				
					 3 changed files with 115 additions and 10 deletions
				
			
		|  | @ -1,7 +1,12 @@ | |||
| using System.IO; | ||||
| using System.Text; | ||||
| using System.Text.Json; | ||||
| using Microsoft.AspNetCore.Builder; | ||||
| using Microsoft.AspNetCore.Diagnostics.HealthChecks; | ||||
| using Microsoft.AspNetCore.Http; | ||||
| using Microsoft.AspNetCore.Http.HttpResults; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.Extensions.Diagnostics.HealthChecks; | ||||
| using TanksServer.GameLogic; | ||||
| using TanksServer.Interactivity; | ||||
| 
 | ||||
|  | @ -25,6 +30,11 @@ internal sealed class Endpoints( | |||
|         app.MapGet("/map", () => mapService.MapNames); | ||||
|         app.MapPost("/map", PostMap); | ||||
|         app.MapGet("/map/{name}", GetMapByName); | ||||
| 
 | ||||
|         app.MapHealthChecks("/health", new HealthCheckOptions | ||||
|         { | ||||
|             ResponseWriter = WriteJsonHealthCheckResponse | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     private Results<BadRequest<string>, NotFound<string>, Ok> PostMap([FromQuery] string name) | ||||
|  | @ -107,4 +117,41 @@ internal sealed class Endpoints( | |||
|         var mapInfo = new MapInfo(prototype.Name, prototype.GetType().Name, preview.Data); | ||||
|         return TypedResults.Ok(mapInfo); | ||||
|     } | ||||
| 
 | ||||
|     private static Task WriteJsonHealthCheckResponse(HttpContext context, HealthReport healthReport) | ||||
|     { | ||||
|         context.Response.ContentType = "application/json; charset=utf-8"; | ||||
| 
 | ||||
|         var options = new JsonWriterOptions { Indented = true }; | ||||
| 
 | ||||
|         using var memoryStream = new MemoryStream(); | ||||
|         using (var jsonWriter = new Utf8JsonWriter(memoryStream, options)) | ||||
|         { | ||||
|             jsonWriter.WriteStartObject(); | ||||
|             jsonWriter.WriteString("status", healthReport.Status.ToString()); | ||||
|             jsonWriter.WriteStartObject("results"); | ||||
| 
 | ||||
|             foreach (var healthReportEntry in healthReport.Entries) | ||||
|             { | ||||
|                 jsonWriter.WriteStartObject(healthReportEntry.Key); | ||||
|                 jsonWriter.WriteString("status", | ||||
|                     healthReportEntry.Value.Status.ToString()); | ||||
|                 jsonWriter.WriteString("description", | ||||
|                     healthReportEntry.Value.Description); | ||||
|                 jsonWriter.WriteStartObject("data"); | ||||
| 
 | ||||
|                 foreach (var item in healthReportEntry.Value.Data) | ||||
|                     jsonWriter.WriteString(item.Key, item.Value.ToString()); | ||||
| 
 | ||||
|                 jsonWriter.WriteEndObject(); | ||||
|                 jsonWriter.WriteEndObject(); | ||||
|             } | ||||
| 
 | ||||
|             jsonWriter.WriteEndObject(); | ||||
|             jsonWriter.WriteEndObject(); | ||||
|         } | ||||
| 
 | ||||
|         return context.Response.WriteAsync( | ||||
|             Encoding.UTF8.GetString(memoryStream.ToArray())); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,13 +1,42 @@ | |||
| using System.Diagnostics; | ||||
| using System.Globalization; | ||||
| using Microsoft.Extensions.Diagnostics.HealthChecks; | ||||
| 
 | ||||
| namespace TanksServer.GameLogic; | ||||
| 
 | ||||
| internal sealed class UpdatesPerSecondCounter(ILogger<UpdatesPerSecondCounter> logger) : ITickStep | ||||
| internal sealed class UpdatesPerSecondCounter( | ||||
|     ILogger<UpdatesPerSecondCounter> logger | ||||
| ) : ITickStep, IHealthCheck | ||||
| { | ||||
|     private static readonly TimeSpan LongTime = TimeSpan.FromSeconds(5); | ||||
|     private static readonly TimeSpan CriticalUpdateTime = TimeSpan.FromMilliseconds(50); | ||||
| 
 | ||||
|     private readonly Stopwatch _long = Stopwatch.StartNew(); | ||||
| 
 | ||||
|     private readonly record struct Statistics( | ||||
|         ulong Updates, | ||||
|         TimeSpan TotalTime, | ||||
|         double AverageUpdatesPerSecond, | ||||
|         TimeSpan MinFrameTime, | ||||
|         TimeSpan AverageFrameTime, | ||||
|         TimeSpan MaxFrameTime) | ||||
|     { | ||||
|         public override string ToString() => | ||||
|             $"{nameof(Updates)}: {Updates}, {nameof(TotalTime)}: {TotalTime}, {nameof(AverageUpdatesPerSecond)}: {AverageUpdatesPerSecond}, {nameof(MinFrameTime)}: {MinFrameTime}, {nameof(AverageFrameTime)}: {AverageFrameTime}, {nameof(MaxFrameTime)}: {MaxFrameTime}"; | ||||
| 
 | ||||
|         public Dictionary<string, object> ToDictionary() => new() | ||||
|         { | ||||
|             [nameof(Updates)] = Updates.ToString(), | ||||
|             [nameof(TotalTime)] = TotalTime.ToString(), | ||||
|             [nameof(AverageUpdatesPerSecond)] = AverageUpdatesPerSecond.ToString(CultureInfo.InvariantCulture), | ||||
|             [nameof(MinFrameTime)] = MinFrameTime.ToString(), | ||||
|             [nameof(AverageFrameTime)] = AverageFrameTime.ToString(), | ||||
|             [nameof(MaxFrameTime)] = MaxFrameTime.ToString() | ||||
|         }; | ||||
|     }; | ||||
| 
 | ||||
|     private Statistics? _currentStatistics = null; | ||||
| 
 | ||||
|     private ulong _updatesSinceLongReset; | ||||
|     private TimeSpan _minFrameTime = TimeSpan.MaxValue; | ||||
|     private TimeSpan _maxFrameTime = TimeSpan.MinValue; | ||||
|  | @ -40,16 +69,20 @@ internal sealed class UpdatesPerSecondCounter(ILogger<UpdatesPerSecondCounter> l | |||
| 
 | ||||
|     private void LogCounters() | ||||
|     { | ||||
|         var time = _long.Elapsed; | ||||
| 
 | ||||
|         _currentStatistics = new Statistics( | ||||
|             _updatesSinceLongReset, | ||||
|             time, | ||||
|             _updatesSinceLongReset / time.TotalSeconds, | ||||
|             _minFrameTime, | ||||
|             time / _updatesSinceLongReset, | ||||
|             _maxFrameTime); | ||||
| 
 | ||||
|         if (!logger.IsEnabled(LogLevel.Debug)) | ||||
|             return; | ||||
| 
 | ||||
|         var time = _long.Elapsed; | ||||
|         var averageTime = Math.Round(time.TotalMilliseconds / _updatesSinceLongReset, 2); | ||||
|         var averageUps = Math.Round(_updatesSinceLongReset / time.TotalSeconds, 2); | ||||
|         var min = Math.Round(_minFrameTime.TotalMilliseconds, 2); | ||||
|         var max = Math.Round(_maxFrameTime.TotalMilliseconds, 2); | ||||
|         logger.LogDebug("count={}, time={}, avg={}ms, ups={}, min={}ms, max={}ms", | ||||
|             _updatesSinceLongReset, time, averageTime, averageUps, min, max); | ||||
|         logger.LogDebug("statistics: {}", _currentStatistics); | ||||
|     } | ||||
| 
 | ||||
|     private void ResetCounters() | ||||
|  | @ -59,4 +92,23 @@ internal sealed class UpdatesPerSecondCounter(ILogger<UpdatesPerSecondCounter> l | |||
|         _minFrameTime = TimeSpan.MaxValue; | ||||
|         _maxFrameTime = TimeSpan.MinValue; | ||||
|     } | ||||
| 
 | ||||
|     public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, | ||||
|         CancellationToken cancellationToken = new()) | ||||
|     { | ||||
|         var stats = _currentStatistics; | ||||
|         if (stats == null) | ||||
|         { | ||||
|             return Task.FromResult( | ||||
|                 HealthCheckResult.Degraded("no statistics available yet - this is expected shortly after start")); | ||||
|         } | ||||
| 
 | ||||
|         if (stats.Value.MaxFrameTime > CriticalUpdateTime) | ||||
|         { | ||||
|             return Task.FromResult(HealthCheckResult.Degraded("max frame time too high", null, | ||||
|                 stats.Value.ToDictionary())); | ||||
|         } | ||||
| 
 | ||||
|         return Task.FromResult(HealthCheckResult.Healthy("", stats.Value.ToDictionary())); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -51,6 +51,9 @@ public static class Program | |||
| 
 | ||||
|         builder.Services.AddHttpLogging(_ => { }); | ||||
| 
 | ||||
|         var healthCheckBuilder = builder.Services.AddHealthChecks(); | ||||
|         healthCheckBuilder.AddCheck<UpdatesPerSecondCounter>("updates check"); | ||||
| 
 | ||||
|         builder.Services.Configure<HostConfiguration>(builder.Configuration.GetSection("Host")); | ||||
|         var hostConfiguration = builder.Configuration.GetSection("Host").Get<HostConfiguration>(); | ||||
|         if (hostConfiguration == null) | ||||
|  | @ -66,12 +69,14 @@ public static class Program | |||
|         builder.Services.AddSingleton<BufferPool>(); | ||||
|         builder.Services.AddSingleton<EmptyTileFinder>(); | ||||
|         builder.Services.AddSingleton<ChangeToRequestedMap>(); | ||||
|         builder.Services.AddSingleton<UpdatesPerSecondCounter>(); | ||||
| 
 | ||||
|         builder.Services.AddHostedService<GameTickWorker>(); | ||||
|         builder.Services.AddHostedService(sp => sp.GetRequiredService<ControlsServer>()); | ||||
|         builder.Services.AddHostedService(sp => sp.GetRequiredService<ClientScreenServer>()); | ||||
| 
 | ||||
|         builder.Services.AddSingleton<ITickStep, ChangeToRequestedMap>(sp => sp.GetRequiredService<ChangeToRequestedMap>()); | ||||
|         builder.Services.AddSingleton<ITickStep, ChangeToRequestedMap>(sp => | ||||
|             sp.GetRequiredService<ChangeToRequestedMap>()); | ||||
|         builder.Services.AddSingleton<ITickStep, MoveBullets>(); | ||||
|         builder.Services.AddSingleton<ITickStep, CollideBullets>(); | ||||
|         builder.Services.AddSingleton<ITickStep, RotateTanks>(); | ||||
|  | @ -82,7 +87,8 @@ public static class Program | |||
|         builder.Services.AddSingleton<ITickStep, SpawnPowerUp>(); | ||||
|         builder.Services.AddSingleton<ITickStep, GeneratePixelsTickStep>(); | ||||
|         builder.Services.AddSingleton<ITickStep, PlayerServer>(sp => sp.GetRequiredService<PlayerServer>()); | ||||
|         builder.Services.AddSingleton<ITickStep, UpdatesPerSecondCounter>(); | ||||
|         builder.Services.AddSingleton<ITickStep, UpdatesPerSecondCounter>(sp => | ||||
|             sp.GetRequiredService<UpdatesPerSecondCounter>()); | ||||
| 
 | ||||
|         builder.Services.AddSingleton<IDrawStep, DrawMapStep>(); | ||||
|         builder.Services.AddSingleton<IDrawStep, DrawPowerUpsStep>(); | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Vinzenz Schroeter
						Vinzenz Schroeter