add health check
This commit is contained in:
parent
de25b69a4b
commit
053bfb0d92
|
@ -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…
Reference in a new issue