feat: add actual metrics using prometheus-net
This commit is contained in:
parent
4a6b5f3b85
commit
54ec469cd9
9 changed files with 134 additions and 56 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
37
Foxnouns.Backend/FoxnounsMetrics.cs
Normal file
37
Foxnouns.Backend/FoxnounsMetrics.cs
Normal 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");
|
||||||
|
}
|
|
@ -1,6 +0,0 @@
|
||||||
namespace Foxnouns.Backend;
|
|
||||||
|
|
||||||
public static class Metrics
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
|
@ -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(_ =>
|
||||||
|
|
66
Foxnouns.Backend/Services/MetricsCollectionService.cs
Normal file
66
Foxnouns.Backend/Services/MetricsCollectionService.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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]
|
||||||
|
|
Loading…
Reference in a new issue