From f3a35bd62a216afe408385feeeb67e92e60eb3e9 Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Fri, 23 May 2025 17:15:19 +0200 Subject: [PATCH] Started implementing metrics system --- .../Configuration/AppConfiguration.cs | 7 ++ .../Metrics/ApplicationMetric.cs | 35 +++++++++ .../Implementations/Metrics/UsersMetric.cs | 27 +++++++ .../Implementations/Startup/CoreStartup.cs | 51 ++++++++++-- Moonlight.ApiServer/Interfaces/IMetric.cs | 9 +++ .../Moonlight.ApiServer.csproj | 4 + .../Services/MetricsBackgroundService.cs | 78 +++++++++++++++++++ 7 files changed, 203 insertions(+), 8 deletions(-) create mode 100644 Moonlight.ApiServer/Implementations/Metrics/ApplicationMetric.cs create mode 100644 Moonlight.ApiServer/Implementations/Metrics/UsersMetric.cs create mode 100644 Moonlight.ApiServer/Interfaces/IMetric.cs create mode 100644 Moonlight.ApiServer/Services/MetricsBackgroundService.cs diff --git a/Moonlight.ApiServer/Configuration/AppConfiguration.cs b/Moonlight.ApiServer/Configuration/AppConfiguration.cs index 22096878..c6b2ffe5 100644 --- a/Moonlight.ApiServer/Configuration/AppConfiguration.cs +++ b/Moonlight.ApiServer/Configuration/AppConfiguration.cs @@ -11,6 +11,7 @@ public class AppConfiguration public DevelopmentConfig Development { get; set; } = new(); public ClientConfig Client { get; set; } = new(); public KestrelConfig Kestrel { get; set; } = new(); + public MetricsData Metrics { get; set; } = new(); public class ClientConfig { @@ -59,4 +60,10 @@ public class AppConfiguration public int UploadLimit { get; set; } = 100; public string AllowedOrigins { get; set; } = "*"; } + + public class MetricsData + { + public bool Enable { get; set; } = false; + public int Interval { get; set; } = 15; + } } \ No newline at end of file diff --git a/Moonlight.ApiServer/Implementations/Metrics/ApplicationMetric.cs b/Moonlight.ApiServer/Implementations/Metrics/ApplicationMetric.cs new file mode 100644 index 00000000..424910a9 --- /dev/null +++ b/Moonlight.ApiServer/Implementations/Metrics/ApplicationMetric.cs @@ -0,0 +1,35 @@ +using System.Diagnostics.Metrics; +using Moonlight.ApiServer.Interfaces; +using Moonlight.ApiServer.Services; + +namespace Moonlight.ApiServer.Implementations.Metrics; + +public class ApplicationMetric : IMetric +{ + private Gauge MemoryUsage; + private Gauge CpuUsage; + private Gauge Uptime; + + public Task Initialize(Meter meter) + { + MemoryUsage = meter.CreateGauge("moonlight_memory_usage"); + CpuUsage = meter.CreateGauge("moonlight_cpu_usage"); + Uptime = meter.CreateGauge("moonlight_uptime"); + + return Task.CompletedTask; + } + + public async Task Run(IServiceProvider provider, CancellationToken cancellationToken) + { + var applicationService = provider.GetRequiredService(); + + var memory = await applicationService.GetMemoryUsage(); + MemoryUsage.Record(memory); + + var uptime = await applicationService.GetUptime(); + Uptime.Record(uptime.TotalSeconds); + + var cpu = await applicationService.GetCpuUsage(); + CpuUsage.Record(cpu); + } +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Implementations/Metrics/UsersMetric.cs b/Moonlight.ApiServer/Implementations/Metrics/UsersMetric.cs new file mode 100644 index 00000000..698c4e8e --- /dev/null +++ b/Moonlight.ApiServer/Implementations/Metrics/UsersMetric.cs @@ -0,0 +1,27 @@ +using System.Diagnostics.Metrics; +using Microsoft.EntityFrameworkCore; +using MoonCore.Extended.Abstractions; +using Moonlight.ApiServer.Database.Entities; +using Moonlight.ApiServer.Interfaces; + +namespace Moonlight.ApiServer.Implementations.Metrics; + +public class UsersMetric : IMetric +{ + private Gauge Users; + + public Task Initialize(Meter meter) + { + Users = meter.CreateGauge("moonlight_users"); + + return Task.CompletedTask; + } + + public async Task Run(IServiceProvider provider, CancellationToken cancellationToken) + { + var usersRepo = provider.GetRequiredService>(); + var count = await usersRepo.Get().CountAsync(cancellationToken: cancellationToken); + + Users.Record(count); + } +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Implementations/Startup/CoreStartup.cs b/Moonlight.ApiServer/Implementations/Startup/CoreStartup.cs index 3f27ab65..6f79c079 100644 --- a/Moonlight.ApiServer/Implementations/Startup/CoreStartup.cs +++ b/Moonlight.ApiServer/Implementations/Startup/CoreStartup.cs @@ -2,8 +2,12 @@ using Microsoft.OpenApi.Models; using Moonlight.ApiServer.Configuration; using Moonlight.ApiServer.Database; using Moonlight.ApiServer.Implementations.Diagnose; +using Moonlight.ApiServer.Implementations.Metrics; using Moonlight.ApiServer.Interfaces; using Moonlight.ApiServer.Plugins; +using Moonlight.ApiServer.Services; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; namespace Moonlight.ApiServer.Implementations.Startup; @@ -13,13 +17,13 @@ public class CoreStartup : IPluginStartup public Task BuildApplication(IServiceProvider serviceProvider, IHostApplicationBuilder builder) { var configuration = serviceProvider.GetRequiredService(); - + #region Api Docs if (configuration.Development.EnableApiDocs) { builder.Services.AddEndpointsApiExplorer(); - + // Configure swagger api specification generator and set the document title for the api docs to use builder.Services.AddSwaggerGen(options => { @@ -27,9 +31,9 @@ public class CoreStartup : IPluginStartup { Title = "Moonlight API" }); - + options.CustomSchemaIds(x => x.FullName); - + options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Name = "Authorization", @@ -54,22 +58,53 @@ public class CoreStartup : IPluginStartup builder.Services.AddSingleton(); #endregion - + + #region Prometheus + + if (configuration.Metrics.Enable) + { + builder.Services.AddSingleton(); + builder.Services.AddHostedService(sp => sp.GetRequiredService()); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + builder.Services.AddOpenTelemetry() + .WithMetrics(providerBuilder => + { + providerBuilder.AddPrometheusExporter(); + providerBuilder.AddAspNetCoreInstrumentation(); + + providerBuilder.AddMeter("moonlight"); + }); + } + + #endregion + return Task.CompletedTask; } public Task ConfigureApplication(IServiceProvider serviceProvider, IApplicationBuilder app) { + var configuration = serviceProvider.GetRequiredService(); + + #region Prometheus + + if(configuration.Metrics.Enable) + app.UseOpenTelemetryPrometheusScrapingEndpoint(); + + #endregion + return Task.CompletedTask; } public Task ConfigureEndpoints(IServiceProvider serviceProvider, IEndpointRouteBuilder routeBuilder) { var configuration = serviceProvider.GetRequiredService(); - - if(configuration.Development.EnableApiDocs) + + if (configuration.Development.EnableApiDocs) routeBuilder.MapSwagger("/api/swagger/{documentName}"); - + return Task.CompletedTask; } } \ No newline at end of file diff --git a/Moonlight.ApiServer/Interfaces/IMetric.cs b/Moonlight.ApiServer/Interfaces/IMetric.cs new file mode 100644 index 00000000..3cd4cace --- /dev/null +++ b/Moonlight.ApiServer/Interfaces/IMetric.cs @@ -0,0 +1,9 @@ +using System.Diagnostics.Metrics; + +namespace Moonlight.ApiServer.Interfaces; + +public interface IMetric +{ + public Task Initialize(Meter meter); + public Task Run(IServiceProvider provider, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Moonlight.ApiServer.csproj b/Moonlight.ApiServer/Moonlight.ApiServer.csproj index caa5256b..e9f4961e 100644 --- a/Moonlight.ApiServer/Moonlight.ApiServer.csproj +++ b/Moonlight.ApiServer/Moonlight.ApiServer.csproj @@ -11,6 +11,7 @@ + @@ -36,6 +37,9 @@ + + + diff --git a/Moonlight.ApiServer/Services/MetricsBackgroundService.cs b/Moonlight.ApiServer/Services/MetricsBackgroundService.cs new file mode 100644 index 00000000..b1e3ec91 --- /dev/null +++ b/Moonlight.ApiServer/Services/MetricsBackgroundService.cs @@ -0,0 +1,78 @@ +using System.Diagnostics.Metrics; +using Moonlight.ApiServer.Configuration; +using Moonlight.ApiServer.Interfaces; + +namespace Moonlight.ApiServer.Services; + +public class MetricsBackgroundService : BackgroundService +{ + private readonly ILogger Logger; + private readonly IServiceProvider ServiceProvider; + private readonly AppConfiguration Configuration; + + private readonly IMetric[] Metrics; + private readonly Meter Meter; + + public MetricsBackgroundService( + IServiceProvider serviceProvider, + IMeterFactory meterFactory, + IEnumerable metrics, + ILogger logger, + AppConfiguration configuration + ) + { + ServiceProvider = serviceProvider; + Logger = logger; + Configuration = configuration; + + Meter = meterFactory.Create("moonlight"); + + Metrics = metrics.ToArray(); + } + + private async Task Initialize() + { + Logger.LogDebug( + "Initializing metrics: {names}", + string.Join(", ", Metrics.Select(x => x.GetType().FullName)) + ); + + foreach (var metric in Metrics) + await metric.Initialize(Meter); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await Initialize(); + + while (!stoppingToken.IsCancellationRequested) + { + using var scope = ServiceProvider.CreateScope(); + + foreach (var metric in Metrics) + { + try + { + await metric.Run(scope.ServiceProvider, stoppingToken); + } + catch (TaskCanceledException) + { + // Ignored + } + catch (Exception e) + { + Logger.LogError( + "An unhandled error occured while collecting metric {name}: {e}", + metric.GetType().FullName, + e + ); + } + } + + await Task.Delay( + TimeSpan.FromSeconds(Configuration.Metrics.Interval), + stoppingToken + ); + } + } +} \ No newline at end of file