feat: add actual metrics using prometheus-net

This commit is contained in:
sam 2024-09-03 17:00:14 +02:00
parent 4a6b5f3b85
commit 54ec469cd9
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
9 changed files with 134 additions and 56 deletions

View file

@ -10,7 +10,7 @@ public class Config
public string MediaBaseUrl { get; set; } = null!; public string MediaBaseUrl { get; set; } = null!;
public string Address => $"http://{Host}:{Port}"; public string Address => $"http://{Host}:{Port}";
public string? MetricsAddress => Logging.MetricsPort != null ? $"http://{Host}:{Logging.MetricsPort}" : null; public string MetricsAddress => $"http://{Host}:{Logging.MetricsPort}";
public LoggingConfig Logging { get; init; } = new(); public LoggingConfig Logging { get; init; } = new();
public DatabaseConfig Database { get; init; } = new(); public DatabaseConfig Database { get; init; } = new();
@ -27,7 +27,8 @@ public class Config
public bool SentryTracing { get; init; } = false; public bool SentryTracing { get; init; } = false;
public double SentryTracesSampleRate { get; init; } = 0.0; public double SentryTracesSampleRate { get; init; } = 0.0;
public bool LogQueries { get; init; } = false; public bool LogQueries { get; init; } = false;
public int? MetricsPort { get; init; } public bool EnableMetrics { get; init; } = false;
public ushort MetricsPort { get; init; } = 5001;
} }
public class DatabaseConfig public class DatabaseConfig

View file

@ -1,30 +1,23 @@
using Foxnouns.Backend.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NodaTime;
namespace Foxnouns.Backend.Controllers; namespace Foxnouns.Backend.Controllers;
[Route("/api/v2/meta")] [Route("/api/v2/meta")]
public class MetaController(DatabaseContext db, IClock clock) : ApiControllerBase public class MetaController : ApiControllerBase
{ {
private const string Repository = "https://codeberg.org/pronounscc/pronouns.cc"; private const string Repository = "https://codeberg.org/pronounscc/pronouns.cc";
[HttpGet] [HttpGet]
[ProducesResponseType<MetaResponse>(StatusCodes.Status200OK)] [ProducesResponseType<MetaResponse>(StatusCodes.Status200OK)]
public async Task<IActionResult> GetMeta() public IActionResult GetMeta()
{ {
var now = clock.GetCurrentInstant();
var users = await db.Users.Select(u => u.LastActive).ToListAsync();
var memberCount = await db.Members.CountAsync();
return Ok(new MetaResponse( return Ok(new MetaResponse(
Repository, BuildInfo.Version, BuildInfo.Hash, memberCount, Repository, BuildInfo.Version, BuildInfo.Hash, (int)FoxnounsMetrics.MemberCount.Value,
new UserInfo( new UserInfo(
users.Count, (int)FoxnounsMetrics.UsersCount.Value,
users.Count(i => i > now - Duration.FromDays(30)), (int)FoxnounsMetrics.UsersActiveMonthCount.Value,
users.Count(i => i > now - Duration.FromDays(7)), (int)FoxnounsMetrics.UsersActiveWeekCount.Value,
users.Count(i => i > now - Duration.FromDays(1)) (int)FoxnounsMetrics.UsersActiveDayCount.Value
)) ))
); );
} }

View file

@ -1,6 +1,3 @@
using App.Metrics;
using App.Metrics.AspNetCore;
using App.Metrics.Formatters.Prometheus;
using Coravel; using Coravel;
using Coravel.Queuing.Interfaces; using Coravel.Queuing.Interfaces;
using Foxnouns.Backend.Database; using Foxnouns.Backend.Database;
@ -9,6 +6,7 @@ using Foxnouns.Backend.Middleware;
using Foxnouns.Backend.Services; using Foxnouns.Backend.Services;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime; using NodaTime;
using Prometheus;
using Serilog; using Serilog;
using Serilog.Events; using Serilog.Events;
using IClock = NodaTime.IClock; using IClock = NodaTime.IClock;
@ -59,32 +57,12 @@ public static class WebApplicationExtensions
return config; return config;
} }
public static WebApplicationBuilder AddMetrics(this WebApplicationBuilder builder) public static WebApplicationBuilder AddMetrics(this WebApplicationBuilder builder, Config config)
{ {
var config = builder.Configuration.Get<Config>() ?? new(); builder.Services.AddMetricServer(o => o.Port = config.Logging.MetricsPort)
var metrics = AppMetrics.CreateDefaultBuilder() .AddSingleton<MetricsCollectionService>();
.OutputMetrics.AsPrometheusPlainText() if (!config.Logging.EnableMetrics)
.Build(); builder.Services.AddHostedService<BackgroundMetricsCollectionService>();
builder.Services.AddSingleton(metrics);
builder.Services.AddSingleton<IMetrics>(metrics);
builder.WebHost
.ConfigureMetrics(metrics)
.UseMetrics(opts =>
{
opts.EndpointOptions = options =>
{
// Metrics must listen on a separate port for security reasons. If no metrics port is set, disable the endpoint entirely.
options.MetricsEndpointEnabled = config.Logging.MetricsPort != null;
options.EnvironmentInfoEndpointEnabled = config.Logging.MetricsPort != null;
options.MetricsTextEndpointEnabled = false;
options.MetricsEndpointOutputFormatter = metrics.OutputMetricsFormatters
.OfType<MetricsPrometheusTextOutputFormatter>().First();
};
})
.UseMetricsWebTracking()
.ConfigureAppMetricsHostingConfiguration(opts => { opts.AllEndpointsPort = config.Logging.MetricsPort; });
return builder; return builder;
} }

View file

@ -6,9 +6,6 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="App.Metrics" Version="4.3.0" />
<PackageReference Include="App.Metrics.AspNetCore.All" Version="4.3.0" />
<PackageReference Include="App.Metrics.Prometheus" Version="4.3.0" />
<PackageReference Include="Coravel" Version="5.0.4" /> <PackageReference Include="Coravel" Version="5.0.4" />
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3"/> <PackageReference Include="EFCore.NamingConventions" Version="8.0.3"/>
<PackageReference Include="EntityFrameworkCore.Exceptions.PostgreSQL" Version="8.1.2" /> <PackageReference Include="EntityFrameworkCore.Exceptions.PostgreSQL" Version="8.1.2" />
@ -25,6 +22,8 @@
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4"/> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="8.0.4"/> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="8.0.4"/>
<PackageReference Include="Npgsql.Json.NET" Version="8.0.3" /> <PackageReference Include="Npgsql.Json.NET" Version="8.0.3" />
<PackageReference Include="prometheus-net" Version="8.2.1" />
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1" />
<PackageReference Include="Sentry.AspNetCore" Version="4.9.0" /> <PackageReference Include="Sentry.AspNetCore" Version="4.9.0" />
<PackageReference Include="Serilog" Version="4.0.1" /> <PackageReference Include="Serilog" Version="4.0.1" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1"/> <PackageReference Include="Serilog.AspNetCore" Version="8.0.1"/>

View file

@ -0,0 +1,37 @@
using Prometheus;
namespace Foxnouns.Backend;
public static class FoxnounsMetrics
{
public static readonly Gauge UsersCount =
Metrics.CreateGauge("foxnouns_user_count", "Number of total users");
public static readonly Gauge UsersActiveMonthCount =
Metrics.CreateGauge("foxnouns_user_count_active_month", "Number of users active in the last month");
public static readonly Gauge UsersActiveWeekCount =
Metrics.CreateGauge("foxnouns_user_count_active_week", "Number of users active in the last week");
public static readonly Gauge UsersActiveDayCount =
Metrics.CreateGauge("foxnouns_user_count_active_day", "Number of users active in the last day");
public static readonly Gauge MemberCount =
Metrics.CreateGauge("foxnouns_member_count", "Number of total members");
public static readonly Summary MetricsCollectionTime =
Metrics.CreateSummary("foxnouns_time_metrics", "Time it took to collect metrics");
public static Gauge ProcessPhysicalMemory =>
Metrics.CreateGauge("foxnouns_process_physical_memory", "Process physical memory");
public static Gauge ProcessVirtualMemory =>
Metrics.CreateGauge("foxnouns_process_virtual_memory", "Process virtual memory");
public static Gauge ProcessPrivateMemory =>
Metrics.CreateGauge("foxnouns_process_private_memory", "Process private memory");
public static Gauge ProcessThreads => Metrics.CreateGauge("foxnouns_process_threads", "Process thread count");
public static Gauge ProcessHandles => Metrics.CreateGauge("foxnouns_process_handles", "Process handle count");
}

View file

@ -1,6 +0,0 @@
namespace Foxnouns.Backend;
public static class Metrics
{
}

View file

@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc;
using Minio; using Minio;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Serialization; using Newtonsoft.Json.Serialization;
using Prometheus;
using Sentry.Extensibility; using Sentry.Extensibility;
// Read version information from .version in the repository root // Read version information from .version in the repository root
@ -18,7 +19,9 @@ var builder = WebApplication.CreateBuilder(args);
var config = builder.AddConfiguration(); var config = builder.AddConfiguration();
builder.AddSerilog().AddMetrics(); builder
.AddSerilog()
.AddMetrics(config);
builder.WebHost builder.WebHost
.UseSentry(opts => .UseSentry(opts =>
@ -89,7 +92,10 @@ app.MapControllers();
app.Urls.Clear(); app.Urls.Clear();
app.Urls.Add(config.Address); app.Urls.Add(config.Address);
if (config.MetricsAddress != null) app.Urls.Add(config.MetricsAddress);
// Make sure metrics are updated whenever Prometheus scrapes them
Metrics.DefaultRegistry.AddBeforeCollectCallback(async ct =>
await app.Services.GetRequiredService<MetricsCollectionService>().CollectMetricsAsync(ct));
// Fire off the periodic tasks loop in the background // Fire off the periodic tasks loop in the background
_ = new Timer(_ => _ = new Timer(_ =>

View file

@ -0,0 +1,66 @@
using System.Diagnostics;
using Foxnouns.Backend.Database;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using Prometheus;
namespace Foxnouns.Backend.Services;
public class MetricsCollectionService(
ILogger logger,
IServiceProvider services,
IClock clock)
{
private readonly ILogger _logger = logger.ForContext<MetricsCollectionService>();
private static readonly Duration Month = Duration.FromDays(30);
private static readonly Duration Week = Duration.FromDays(7);
private static readonly Duration Day = Duration.FromDays(1);
public async Task CollectMetricsAsync(CancellationToken ct = default)
{
var timer = FoxnounsMetrics.MetricsCollectionTime.NewTimer();
var now = clock.GetCurrentInstant();
await using var scope = services.CreateAsyncScope();
await using var db = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
var users = await db.Users.Where(u => !u.Deleted).Select(u => u.LastActive).ToListAsync(ct);
FoxnounsMetrics.UsersCount.Set(users.Count);
FoxnounsMetrics.UsersActiveMonthCount.Set(users.Count(i => i > now - Month));
FoxnounsMetrics.UsersActiveWeekCount.Set(users.Count(i => i > now - Week));
FoxnounsMetrics.UsersActiveDayCount.Set(users.Count(i => i > now - Day));
var memberCount = await db.Members.Include(m => m.User)
.Where(m => !m.Unlisted && !m.User.ListHidden && !m.User.Deleted).CountAsync(ct);
FoxnounsMetrics.MemberCount.Set(memberCount);
var process = Process.GetCurrentProcess();
FoxnounsMetrics.ProcessPhysicalMemory.Set(process.WorkingSet64);
FoxnounsMetrics.ProcessVirtualMemory.Set(process.VirtualMemorySize64);
FoxnounsMetrics.ProcessPrivateMemory.Set(process.PrivateMemorySize64);
FoxnounsMetrics.ProcessThreads.Set(process.Threads.Count);
FoxnounsMetrics.ProcessHandles.Set(process.HandleCount);
_logger.Information("Collected metrics in {DurationMilliseconds} ms",
timer.ObserveDuration().TotalMilliseconds);
}
}
public class BackgroundMetricsCollectionService(ILogger logger, MetricsCollectionService innerService)
: BackgroundService
{
private readonly ILogger _logger = logger.ForContext<BackgroundMetricsCollectionService>();
protected override async Task ExecuteAsync(CancellationToken ct)
{
_logger.Information("Metrics are disabled, periodically collecting metrics manually");
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
while (await timer.WaitForNextTickAsync(ct))
{
_logger.Debug("Collecting metrics");
await innerService.CollectMetricsAsync(ct);
}
}
}

View file

@ -20,7 +20,11 @@ SentryTracing = true
SentryTracesSampleRate = 1.0 SentryTracesSampleRate = 1.0
; Whether to log SQL queries. Note that this is very verbose. Defaults to false. ; Whether to log SQL queries. Note that this is very verbose. Defaults to false.
LogQueries = false LogQueries = false
; The port the /metrics endpoint will listen on. If not set, metrics will be disabled. ; Whether metrics are enabled. If this is set to true, Foxnouns.NET will rely on Prometheus scraping metrics to update stats.
; If set to false, a background service will be used instead. Does not actually disable the /metrics endpoint.
; Defaults to false.
EnableMetrics = true
; The port the /metrics endpoint will listen on. Defaults to 5001.
MetricsPort = 5001 MetricsPort = 5001
[Database] [Database]