add metrics (without reporting, for now)

This commit is contained in:
sam 2024-08-15 17:23:56 +02:00
parent 5585ffd6ea
commit 14b132e466
Signed by: sam
GPG key ID: 5F3C3C1B3166639D
9 changed files with 183 additions and 9 deletions

View file

@ -1,12 +1,12 @@
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using App.Metrics;
using Catalogger.Backend.Cache; using Catalogger.Backend.Cache;
using Catalogger.Backend.Database; using Catalogger.Backend.Database;
using Catalogger.Backend.Extensions; using Catalogger.Backend.Extensions;
using Humanizer; using Humanizer;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime;
using Remora.Commands.Attributes; using Remora.Commands.Attributes;
using Remora.Commands.Groups; using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Objects;
@ -15,6 +15,7 @@ using Remora.Discord.Commands.Feedback.Services;
using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Gateway; using Remora.Discord.Gateway;
using Remora.Results; using Remora.Results;
using IClock = NodaTime.IClock;
using IResult = Remora.Results.IResult; using IResult = Remora.Results.IResult;
namespace Catalogger.Backend.Bot.Commands; namespace Catalogger.Backend.Bot.Commands;
@ -22,11 +23,11 @@ namespace Catalogger.Backend.Bot.Commands;
[Group("catalogger")] [Group("catalogger")]
public class MetaCommands( public class MetaCommands(
IClock clock, IClock clock,
DatabaseContext db, IMetrics metrics,
DiscordGatewayClient client, DiscordGatewayClient client,
IFeedbackService feedbackService,
GuildCacheService guildCache, GuildCacheService guildCache,
ChannelCacheService channelCache, ChannelCacheService channelCache,
IFeedbackService feedbackService,
IDiscordRestChannelAPI channelApi) : CommandGroup IDiscordRestChannelAPI channelApi) : CommandGroup
{ {
[Command("ping")] [Command("ping")]
@ -37,7 +38,6 @@ public class MetaCommands(
var msg = await feedbackService.SendContextualAsync("...").GetOrThrow(); var msg = await feedbackService.SendContextualAsync("...").GetOrThrow();
var elapsed = clock.GetCurrentInstant() - t1; var elapsed = clock.GetCurrentInstant() - t1;
var messageCount = await db.Messages.CountAsync();
var process = Process.GetCurrentProcess(); var process = Process.GetCurrentProcess();
var memoryUsage = process.WorkingSet64; var memoryUsage = process.WorkingSet64;
@ -49,11 +49,19 @@ public class MetaCommands(
inline: true); inline: true);
embed.AddField("Memory usage", memoryUsage.Bytes().Humanize(), inline: true); embed.AddField("Memory usage", memoryUsage.Bytes().Humanize(), inline: true);
var messagesReceived = metrics.Snapshot.GetForContext("Bot").Meters
.FirstOrDefault(m => m.MultidimensionalName == CataloggerMetrics.MessagesReceived.Name)?.Value;
if (messagesReceived != null)
embed.AddField("Messages received", $"{messagesReceived.OneMinuteRate * 60:F1}/m", true);
var messageCount = metrics.Snapshot.GetForContext("Bot").Gauges
.FirstOrDefault(m => m.MultidimensionalName == CataloggerMetrics.MessagesStored.Name)?.Value ?? 0;
embed.AddField("Numbers", embed.AddField("Numbers",
$"{messageCount:N0} messages from {guildCache.Size:N0} servers\nCached {channelCache.Size:N0} channels", $"{messageCount:N0} messages from {guildCache.Size:N0} servers\nCached {channelCache.Size:N0} channels",
inline: false); inline: false);
List<IEmbed> embeds = [embed.Build().GetOrThrow()]; IEmbed[] embeds = [embed.Build().GetOrThrow()];
return (Result)await channelApi.EditMessageAsync(msg.ChannelID, msg.ID, content: "", embeds: embeds); return (Result)await channelApi.EditMessageAsync(msg.ChannelID, msg.ID, content: "", embeds: embeds);
} }

View file

@ -1,4 +1,5 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using App.Metrics;
using Catalogger.Backend.Cache; using Catalogger.Backend.Cache;
using Catalogger.Backend.Database; using Catalogger.Backend.Database;
using Catalogger.Backend.Database.Models; using Catalogger.Backend.Database.Models;
@ -20,7 +21,8 @@ public class MessageCreateResponder(
DatabaseContext db, DatabaseContext db,
MessageRepository messageRepository, MessageRepository messageRepository,
UserCacheService userCache, UserCacheService userCache,
PkMessageHandler pkMessageHandler) PkMessageHandler pkMessageHandler,
IMetrics metrics)
: IResponder<IMessageCreate> : IResponder<IMessageCreate>
{ {
private readonly ILogger _logger = logger.ForContext<MessageCreateResponder>(); private readonly ILogger _logger = logger.ForContext<MessageCreateResponder>();
@ -29,6 +31,7 @@ public class MessageCreateResponder(
public async Task<Result> RespondAsync(IMessageCreate msg, CancellationToken ct = default) public async Task<Result> RespondAsync(IMessageCreate msg, CancellationToken ct = default)
{ {
userCache.UpdateUser(msg.Author); userCache.UpdateUser(msg.Author);
metrics.Measure.Meter.Mark(CataloggerMetrics.MessagesReceived);
if (!msg.GuildID.IsDefined()) if (!msg.GuildID.IsDefined())
{ {

View file

@ -8,6 +8,8 @@ namespace Catalogger.Backend.Cache;
public class UserCacheService(IDiscordRestUserAPI userApi) public class UserCacheService(IDiscordRestUserAPI userApi)
{ {
private readonly ConcurrentDictionary<Snowflake, IUser> _cache = new(); private readonly ConcurrentDictionary<Snowflake, IUser> _cache = new();
public int Size => _cache.Count;
public async Task<IUser?> GetUserAsync(Snowflake userId) public async Task<IUser?> GetUserAsync(Snowflake userId)
{ {

View file

@ -7,6 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="App.Metrics" Version="4.3.0" />
<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" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.7" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.7" />
@ -27,6 +28,8 @@
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" /> <PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" /> <PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="StackExchange.Redis" Version="2.8.0" />
<PackageReference Include="StackExchange.Redis.Extensions.System.Text.Json" Version="10.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
</ItemGroup> </ItemGroup>

View file

@ -0,0 +1,88 @@
using App.Metrics;
using App.Metrics.Gauge;
using App.Metrics.Meter;
using App.Metrics.Timer;
namespace Catalogger.Backend;
public static class CataloggerMetrics
{
public static MeterOptions MessagesReceived => new()
{
Name = "Messages received",
MeasurementUnit = Unit.Events,
RateUnit = TimeUnit.Seconds,
Context = "Bot"
};
public static GaugeOptions GuildsCached => new()
{
Name = "Guilds cached",
MeasurementUnit = Unit.Items,
Context = "Bot"
};
public static GaugeOptions ChannelsCached => new()
{
Name = "Channels cached",
MeasurementUnit = Unit.Items,
Context = "Bot"
};
public static GaugeOptions UsersCached => new()
{
Name = "Users cached",
MeasurementUnit = Unit.Items,
Context = "Bot"
};
public static GaugeOptions MessagesStored => new()
{
Name = "Messages stored",
MeasurementUnit = Unit.Items,
Context = "Bot"
};
public static TimerOptions MetricsCollectionTime => new()
{
Name = "Metrics collection time",
MeasurementUnit = Unit.Events,
DurationUnit = TimeUnit.Milliseconds,
Context = "Bot"
};
public static GaugeOptions ProcessPhysicalMemory => new()
{
Name = "Process physical memory",
MeasurementUnit = Unit.Bytes,
Context = "Process"
};
public static GaugeOptions ProcessVirtualMemory => new()
{
Name = "Process virtual memory",
MeasurementUnit = Unit.Bytes,
Context = "Process"
};
public static GaugeOptions ProcessPrivateMemory => new()
{
Name = "Process private memory",
MeasurementUnit = Unit.Bytes,
Context = "Process"
};
public static GaugeOptions ProcessThreads => new()
{
Name = "Process thread count",
MeasurementUnit = Unit.Threads,
Context = "Process"
};
public static GaugeOptions ProcessHandles => new()
{
Name = "Process handle count",
MeasurementUnit = Unit.Items,
Context = "Process"
};
}

View file

@ -8,13 +8,13 @@ public class Config
public DatabaseConfig Database { get; init; } = new(); public DatabaseConfig Database { get; init; } = new();
public DiscordConfig Discord { get; init; } = new(); public DiscordConfig Discord { get; init; } = new();
public WebConfig Web { get; init; } = new(); public WebConfig Web { get; init; } = new();
public class LoggingConfig public class LoggingConfig
{ {
public LogEventLevel LogEventLevel { get; init; } = LogEventLevel.Debug; public LogEventLevel LogEventLevel { get; init; } = LogEventLevel.Debug;
public bool LogQueries { get; init; } = false; public bool LogQueries { get; init; } = false;
} }
public class DatabaseConfig public class DatabaseConfig
{ {
public string Url { get; init; } = string.Empty; public string Url { get; init; } = string.Empty;
@ -37,7 +37,9 @@ public class Config
{ {
public string Host { get; init; } = "localhost"; public string Host { get; init; } = "localhost";
public int Port { get; init; } = 5000; public int Port { get; init; } = 5000;
public int? MetricsPort { get; init; }
public string BaseUrl { get; init; } = null!; public string BaseUrl { get; init; } = null!;
public string Address => $"http://{Host}:{Port}"; public string Address => $"http://{Host}:{Port}";
public string MetricsAddress => $"http://{Host}:{MetricsPort ?? Port}";
} }
} }

View file

@ -75,7 +75,8 @@ public static class StartupExtensions
.AddScoped<MessageRepository>() .AddScoped<MessageRepository>()
.AddSingleton<WebhookExecutorService>() .AddSingleton<WebhookExecutorService>()
.AddSingleton<PkMessageHandler>() .AddSingleton<PkMessageHandler>()
.AddSingleton(InMemoryDataService<Snowflake, ChannelCommandData>.Instance); .AddSingleton(InMemoryDataService<Snowflake, ChannelCommandData>.Instance)
.AddHostedService<MetricsCollectionService>();
public static async Task Initialize(this WebApplication app) public static async Task Initialize(this WebApplication app)
{ {

View file

@ -1,3 +1,4 @@
using App.Metrics;
using Catalogger.Backend.Bot.Commands; using Catalogger.Backend.Bot.Commands;
using Catalogger.Backend.Database; using Catalogger.Backend.Database;
using Catalogger.Backend.Extensions; using Catalogger.Backend.Extensions;
@ -49,6 +50,11 @@ builder.Host
.AddInteractionGroup<ChannelCommandsComponents>() .AddInteractionGroup<ChannelCommandsComponents>()
); );
// Add metrics
// TODO: add actual reporter
var metricsBuilder = AppMetrics.CreateDefaultBuilder();
builder.Services.AddSingleton<IMetrics>(metricsBuilder.Build());
builder.Services builder.Services
.AddDbContext<DatabaseContext>() .AddDbContext<DatabaseContext>()
.AddCustomServices() .AddCustomServices()

View file

@ -0,0 +1,61 @@
using System.Diagnostics;
using App.Metrics;
using Catalogger.Backend.Cache;
using Catalogger.Backend.Database;
using Humanizer;
using Microsoft.EntityFrameworkCore;
using NodaTime.Extensions;
namespace Catalogger.Backend.Services;
public class MetricsCollectionService(
ILogger logger,
GuildCacheService guildCache,
ChannelCacheService channelCache,
UserCacheService userCache,
IMetrics metrics,
IServiceProvider services) : BackgroundService
{
private readonly ILogger _logger = logger.ForContext<MetricsCollectionService>();
private async Task CollectMetricsAsync()
{
var stopwatch = new Stopwatch();
stopwatch.Start();
await using var scope = services.CreateAsyncScope();
await using var db = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
var messageCount = await db.Messages.CountAsync();
metrics.Measure.Gauge.SetValue(CataloggerMetrics.GuildsCached, guildCache.Size);
metrics.Measure.Gauge.SetValue(CataloggerMetrics.ChannelsCached, channelCache.Size);
metrics.Measure.Gauge.SetValue(CataloggerMetrics.UsersCached, userCache.Size);
metrics.Measure.Gauge.SetValue(CataloggerMetrics.MessagesStored, messageCount);
var process = Process.GetCurrentProcess();
metrics.Measure.Gauge.SetValue(CataloggerMetrics.ProcessPhysicalMemory, process.WorkingSet64);
metrics.Measure.Gauge.SetValue(CataloggerMetrics.ProcessVirtualMemory, process.VirtualMemorySize64);
metrics.Measure.Gauge.SetValue(CataloggerMetrics.ProcessPrivateMemory, process.PrivateMemorySize64);
metrics.Measure.Gauge.SetValue(CataloggerMetrics.ProcessThreads, process.Threads.Count);
metrics.Measure.Gauge.SetValue(CataloggerMetrics.ProcessHandles, process.HandleCount);
stopwatch.Stop();
metrics.Measure.Timer.Time(CataloggerMetrics.MetricsCollectionTime, stopwatch.ElapsedMilliseconds);
_logger.Information("Collected metrics in {Duration}", stopwatch.ElapsedDuration());
await Task.WhenAll(((IMetricsRoot)metrics).ReportRunner.RunAllAsync());
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(1.Minutes());
while (await timer.WaitForNextTickAsync(stoppingToken))
{
_logger.Debug("Collecting periodic metrics");
await CollectMetricsAsync();
_logger.Debug("Reported metrics to backend");
}
}
}