Skip to content

Commit

Permalink
Implement a /health endpoint (lighttube-org#170)
Browse files Browse the repository at this point in the history
* Implement health managing

* Consider cache hits
  • Loading branch information
kuylar authored Sep 14, 2024
1 parent cdbfc9f commit c1ea1b1
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 3 deletions.
10 changes: 10 additions & 0 deletions LightTube/ApiModels/HealthResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using InnerTube.Models;

namespace LightTube.ApiModels;

public class HealthResponse
{
public int VideoHealth { get; set; }
public double AveragePlayerResponseTime { get; set; }
public CacheStats CacheStats { get; set; }
}
19 changes: 18 additions & 1 deletion LightTube/Controllers/ApiController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Net;
using System.Diagnostics;
using System.Net;
using System.Text.RegularExpressions;
using InnerTube;
using InnerTube.Models;
Expand All @@ -9,6 +10,7 @@
using LightTube.Attributes;
using LightTube.Database;
using LightTube.Database.Models;
using LightTube.Health;
using LightTube.Localization;
using Microsoft.AspNetCore.Mvc;
using Endpoint = InnerTube.Protobuf.Endpoint;
Expand Down Expand Up @@ -37,6 +39,15 @@ public LightTubeInstanceInfo GetInstanceInfo() =>
}
};

[Route("health")]
public HealthResponse GetHealth() =>
new()
{
VideoHealth = (int)HealthManager.GetHealthPercentage(),
AveragePlayerResponseTime = HealthManager.GetAveragePlayerResponseTime(),
CacheStats = innerTube.GetPlayerCacheStats()
};

private ApiResponse<T> Error<T>(string message, int code, HttpStatusCode statusCode)
{
Response.StatusCode = (int)statusCode;
Expand All @@ -55,11 +66,15 @@ public async Task<ApiResponse<InnerTubePlayer>> GetPlayerInfo(string? id, bool c
if (!videoIdRegex.IsMatch(id) || id.Length != 11)
return Error<InnerTubePlayer>($"Invalid video ID: {id}", 400, HttpStatusCode.BadRequest);

Stopwatch sp = Stopwatch.StartNew();
try
{
InnerTubePlayer player = await innerTube.GetVideoPlayerAsync(id, contentCheckOk,
HttpContext.GetInnerTubeLanguage(),
HttpContext.GetInnerTubeRegion());
sp.Stop();
// we dont check if the video id is correct here, might be an issue for the future
HealthManager.PushVideoResponse(id, true, sp.ElapsedMilliseconds);

DatabaseUser? user = await DatabaseManager.Oauth2.GetUserFromHttpRequest(Request);
ApiUserData? userData = ApiUserData.GetFromDatabaseUser(user);
Expand All @@ -69,6 +84,8 @@ public async Task<ApiResponse<InnerTubePlayer>> GetPlayerInfo(string? id, bool c
}
catch (Exception e)
{
sp.Stop();
HealthManager.PushVideoResponse(id, false, sp.ElapsedMilliseconds);
return Error<InnerTubePlayer>(e.Message, 500, HttpStatusCode.InternalServerError);
}
}
Expand Down
11 changes: 9 additions & 2 deletions LightTube/Controllers/YoutubeController.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
using System.Text.Json;
using System.Diagnostics;
using System.Text.Json;
using InnerTube;
using InnerTube.Models;
using InnerTube.Protobuf.Params;
using InnerTube.Protobuf.Responses;
using LightTube.Contexts;
using LightTube.Database;
using LightTube.Database.Models;
using LightTube.Health;
using LightTube.Localization;
using Microsoft.AspNetCore.Mvc;
using Serilog;
using Endpoint = InnerTube.Protobuf.Endpoint;

namespace LightTube.Controllers;
Expand All @@ -19,6 +20,7 @@ public class YoutubeController(SimpleInnerTubeClient innerTube, HttpClient clien
public async Task<IActionResult> Embed(string v, bool contentCheckOk, bool compatibility = false,
bool audioOnly = false)
{
Stopwatch sp = Stopwatch.StartNew();
InnerTubePlayer? player;
Exception? e;
try
Expand All @@ -32,6 +34,7 @@ public async Task<IActionResult> Embed(string v, bool contentCheckOk, bool compa
player = null;
e = ex;
}
sp.Stop();

SponsorBlockSegment[] sponsors;
try
Expand All @@ -48,6 +51,7 @@ public async Task<IActionResult> Embed(string v, bool contentCheckOk, bool compa

InnerTubeVideo video = await innerTube.GetVideoDetailsAsync(v, contentCheckOk, null, null, null,
language: HttpContext.GetInnerTubeLanguage(), region: HttpContext.GetInnerTubeRegion());
HealthManager.PushVideoResponse(v, player != null, sp.ElapsedMilliseconds);
if (player is null || e is not null)
return View(new EmbedContext(HttpContext, e ?? new Exception("player is null"), video));
return View(new EmbedContext(HttpContext, player, video, compatibility, sponsors, audioOnly));
Expand All @@ -57,6 +61,7 @@ public async Task<IActionResult> Embed(string v, bool contentCheckOk, bool compa
public async Task<IActionResult> Watch(string v, string? list, bool contentCheckOk, bool compatibility = false,
bool audioOnly = false)
{
Stopwatch sp = Stopwatch.StartNew();
InnerTubePlayer? player;
Exception? e;
bool localPlaylist = list?.StartsWith("LT-PL") ?? false;
Expand All @@ -77,9 +82,11 @@ public async Task<IActionResult> Watch(string v, string? list, bool contentCheck
player = null;
e = ex;
}
sp.Stop();

InnerTubeVideo video = await innerTube.GetVideoDetailsAsync(v, contentCheckOk, localPlaylist ? null : list,
null, null, language: HttpContext.GetInnerTubeLanguage(), region: HttpContext.GetInnerTubeRegion());
HealthManager.PushVideoResponse(v, player != null, sp.ElapsedMilliseconds);
ContinuationResponse? comments = null;

if (HttpContext.GetDefaultCompatibility())
Expand Down
32 changes: 32 additions & 0 deletions LightTube/Health/HealthManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace LightTube.Health;

public static class HealthManager
{
private static List<KeyValuePair<string, bool>> videoStatuses = [];
private static List<long> playerResponseTimes = [];

public static void PushVideoResponse(string videoId, bool isSuccess, long playerResponseTime)
{
// don't include cache hits
if (playerResponseTime < 50) return;

// if entry with the same videoId exists, remove it
videoStatuses.RemoveAll(x => x.Key == videoId);

// only keep last 100 requests
if (videoStatuses.Count >= 100) videoStatuses.RemoveAt(0);
if (playerResponseTimes.Count >= 100) playerResponseTimes.RemoveAt(0);

playerResponseTimes.Add(playerResponseTime);
videoStatuses.Add(new KeyValuePair<string, bool>(videoId, isSuccess));
}

public static float GetHealthPercentage() =>
Math.Clamp(MathF.Round((float)videoStatuses.Count(x => x.Value) / Math.Max(videoStatuses.Count, 1) * 100), 0, 100);

public static double GetAveragePlayerResponseTime()
{
if (playerResponseTimes.Count == 0) return 0;
return playerResponseTimes.Average();
}
}

0 comments on commit c1ea1b1

Please sign in to comment.