Catalogger.NET/Catalogger.Backend/Bot/Commands/MetaCommands.cs

107 lines
No EOL
4 KiB
C#

using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Web;
using Catalogger.Backend.Cache.InMemoryCache;
using Catalogger.Backend.Extensions;
using Humanizer;
using Humanizer.Localisation;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
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;
[Group("catalogger")]
public class MetaCommands(
ILogger logger,
IClock clock,
Config config,
DiscordGatewayClient client,
IFeedbackService feedbackService,
GuildCache guildCache,
ChannelCache channelCache,
IDiscordRestChannelAPI channelApi) : CommandGroup
{
private readonly ILogger _logger = logger.ForContext<MetaCommands>();
private readonly HttpClient _client = new();
[Command("ping")]
[Description("Ping pong! See the bot's latency")]
public async Task<IResult> PingAsync()
{
var t1 = clock.GetCurrentInstant();
var msg = await feedbackService.SendContextualAsync("...").GetOrThrow();
var elapsed = clock.GetCurrentInstant() - t1;
var process = Process.GetCurrentProcess();
var memoryUsage = process.WorkingSet64;
var embed = new EmbedBuilder()
.WithColour(DiscordUtils.Purple)
.WithFooter($"{RuntimeInformation.FrameworkDescription} on {RuntimeInformation.RuntimeIdentifier}")
.WithCurrentTimestamp();
embed.AddField("Ping", $"Gateway: {client.Latency.Humanize()}\nAPI: {elapsed.ToTimeSpan().Humanize()}",
inline: true);
embed.AddField("Memory usage", memoryUsage.Bytes().Humanize(), inline: true);
var messageRate = await MessagesRate();
embed.AddField("Messages received",
messageRate != null
? $"{messageRate / 5:F1}/m\n({CataloggerMetrics.MessagesReceived.Value:N0} since last restart)"
: $"{CataloggerMetrics.MessagesReceived.Value:N0} since last restart",
true);
embed.AddField("Numbers",
$"{CataloggerMetrics.MessagesStored.Value:N0} messages " +
$"from {guildCache.Size:N0} servers\nCached {channelCache.Size:N0} channels",
true);
embed.AddField("Uptime",
$"{(CataloggerMetrics.Startup - clock.GetCurrentInstant()).Prettify(TimeUnit.Second)}\n" +
$"since <t:{CataloggerMetrics.Startup.ToUnixTimeSeconds()}:F>",
true);
IEmbed[] embeds = [embed.Build().GetOrThrow()];
return (Result)await channelApi.EditMessageAsync(msg.ChannelID, msg.ID, content: "", embeds: embeds);
}
// TODO: add more checks around response format, configurable prometheus endpoint
private async Task<double?> MessagesRate()
{
if (!config.Logging.EnableMetrics) return null;
try
{
var query = HttpUtility.UrlEncode("delta(catalogger_received_messages[5m])");
var resp = await _client.GetAsync($"http://localhost:9090/api/v1/query?query={query}");
resp.EnsureSuccessStatusCode();
var data = await resp.Content.ReadFromJsonAsync<PrometheusResponse>();
var rawNumber = (data?.data.result[0].value[1] as JsonElement?)?.GetString();
return double.TryParse(rawNumber, out var rate) ? rate : null;
}
catch (Exception e)
{
_logger.Warning(e, "Failed querying Prometheus for message rate");
return null;
}
}
// ReSharper disable InconsistentNaming, ClassNeverInstantiated.Local
private record PrometheusResponse(PrometheusData data);
private record PrometheusData(PrometheusResult[] result);
private record PrometheusResult(object[] value);
// ReSharper restore InconsistentNaming, ClassNeverInstantiated.Local
}