add metrics (without reporting, for now)
This commit is contained in:
parent
5585ffd6ea
commit
14b132e466
9 changed files with 183 additions and 9 deletions
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
88
Catalogger.Backend/CataloggerMetrics.cs
Normal file
88
Catalogger.Backend/CataloggerMetrics.cs
Normal 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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
61
Catalogger.Backend/Services/MetricsCollectionService.cs
Normal file
61
Catalogger.Backend/Services/MetricsCollectionService.cs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue