// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EpicGames.Horde.Server;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace HordeServer.Server;
///
/// ASP.NET view model for server status updates
///
public class ServerStatusUpdatesViewModel
{
///
/// Status for each subsystem
///
public IReadOnlyList SubsystemStatuses { get; init; } = Array.Empty();
///
/// Format a date/time to human-readable relative date
///
/// Date/time to convert
/// Relative time
public static string ToRelativeTime(DateTimeOffset dateTime)
{
TimeSpan timeSpan = DateTime.UtcNow - dateTime;
if (timeSpan <= TimeSpan.FromSeconds(60))
{
return $"{timeSpan.Seconds} seconds ago";
}
if (timeSpan <= TimeSpan.FromMinutes(60))
{
return $"{timeSpan.Minutes} minutes ago";
}
if (timeSpan <= TimeSpan.FromHours(24))
{
return $"{timeSpan.Hours} hours ago";
}
if (timeSpan <= TimeSpan.FromDays(30))
{
return $"{timeSpan.Days} days ago";
}
if (timeSpan <= TimeSpan.FromDays(365))
{
return $"{timeSpan.Days / 30} months ago";
}
return $"{timeSpan.Days / 365} years ago";
}
}
///
/// Controller managing server status
///
[ApiController]
[Authorize]
[Tags("Server")]
public class ServerStatusController : Controller
{
private readonly ServerStatusService _serverStatus;
///
/// Constructor
///
public ServerStatusController(ServerStatusService serverStatus)
{
_serverStatus = serverStatus;
}
///
/// Get the server status of Horde's internal subsystems
///
/// Http result
[HttpGet]
[Route("/api/v1/server/status")]
[ProducesResponseType(typeof(ServerStatusResponse), 200)]
public async Task> GetUpdatesAsync([FromQuery] string? format = null)
{
IReadOnlyList subsystemStatuses = await _serverStatus.GetSubsystemStatusesAsync();
if (format == "html")
{
return GetUpdatesHtml(subsystemStatuses);
}
return new ServerStatusResponse
{
Statuses = subsystemStatuses.Select(x =>
{
return new ServerStatusSubsystem()
{
Name = x.Name,
Updates = x.Updates.Select(
u => new ServerStatusUpdate()
{
Result = ConvertSubsystemResult(u.Result),
Message = u.Message ?? "Operating normally.",
UpdatedAt = u.UpdatedAt
}).ToArray()
};
}).ToArray(),
};
}
private ActionResult GetUpdatesHtml(IReadOnlyList subsystemStatuses)
{
return View("~/Server/ServerStatusUpdates.cshtml", new ServerStatusUpdatesViewModel
{
SubsystemStatuses = subsystemStatuses
});
}
private static ServerStatusResult ConvertSubsystemResult(HealthStatus result)
{
return result switch
{
HealthStatus.Healthy => ServerStatusResult.Healthy,
HealthStatus.Unhealthy => ServerStatusResult.Unhealthy,
HealthStatus.Degraded => ServerStatusResult.Degraded,
_ => throw new Exception($"Unknown result: {result}")
};
}
}