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.Diagnostics;
using System.Runtime.InteropServices;
using App.Metrics;
using Catalogger.Backend.Cache;
using Catalogger.Backend.Database;
using Catalogger.Backend.Extensions;
using Humanizer;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects;
@ -15,6 +15,7 @@ using Remora.Discord.Commands.Feedback.Services;
using Remora.Discord.Extensions.Embeds;
using Remora.Discord.Gateway;
using Remora.Results;
using IClock = NodaTime.IClock;
using IResult = Remora.Results.IResult;
namespace Catalogger.Backend.Bot.Commands;
@ -22,11 +23,11 @@ namespace Catalogger.Backend.Bot.Commands;
[Group("catalogger")]
public class MetaCommands(
IClock clock,
DatabaseContext db,
IMetrics metrics,
DiscordGatewayClient client,
IFeedbackService feedbackService,
GuildCacheService guildCache,
ChannelCacheService channelCache,
IFeedbackService feedbackService,
IDiscordRestChannelAPI channelApi) : CommandGroup
{
[Command("ping")]
@ -37,7 +38,6 @@ public class MetaCommands(
var msg = await feedbackService.SendContextualAsync("...").GetOrThrow();
var elapsed = clock.GetCurrentInstant() - t1;
var messageCount = await db.Messages.CountAsync();
var process = Process.GetCurrentProcess();
var memoryUsage = process.WorkingSet64;
@ -49,11 +49,19 @@ public class MetaCommands(
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",
$"{messageCount:N0} messages from {guildCache.Size:N0} servers\nCached {channelCache.Size:N0} channels",
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);
}

View file

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

View file

@ -9,6 +9,8 @@ public class UserCacheService(IDiscordRestUserAPI userApi)
{
private readonly ConcurrentDictionary<Snowflake, IUser> _cache = new();
public int Size => _cache.Count;
public async Task<IUser?> GetUserAsync(Snowflake userId)
{
if (_cache.TryGetValue(userId, out var user)) return user;

View file

@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="App.Metrics" Version="4.3.0" />
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3" />
<PackageReference Include="EntityFrameworkCore.Exceptions.PostgreSQL" Version="8.1.2" />
<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.Extensions.Hosting" Version="8.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"/>
</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

@ -37,7 +37,9 @@ public class Config
{
public string Host { get; init; } = "localhost";
public int Port { get; init; } = 5000;
public int? MetricsPort { get; init; }
public string BaseUrl { get; init; } = null!;
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>()
.AddSingleton<WebhookExecutorService>()
.AddSingleton<PkMessageHandler>()
.AddSingleton(InMemoryDataService<Snowflake, ChannelCommandData>.Instance);
.AddSingleton(InMemoryDataService<Snowflake, ChannelCommandData>.Instance)
.AddHostedService<MetricsCollectionService>();
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.Database;
using Catalogger.Backend.Extensions;
@ -49,6 +50,11 @@ builder.Host
.AddInteractionGroup<ChannelCommandsComponents>()
);
// Add metrics
// TODO: add actual reporter
var metricsBuilder = AppMetrics.CreateDefaultBuilder();
builder.Services.AddSingleton<IMetrics>(metricsBuilder.Build());
builder.Services
.AddDbContext<DatabaseContext>()
.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");
}
}
}