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

155 lines
5.6 KiB
C#
Raw Normal View History

// Copyright (C) 2021-present sam (starshines.gay)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
2024-08-13 13:08:50 +02:00
using System.ComponentModel;
2024-08-13 16:48:54 +02:00
using System.Diagnostics;
2024-08-13 17:02:11 +02:00
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Web;
using Catalogger.Backend.Cache.InMemoryCache;
2024-08-13 13:08:50 +02:00
using Catalogger.Backend.Extensions;
2024-08-13 16:48:54 +02:00
using Humanizer;
using Humanizer.Localisation;
2024-08-13 13:08:50 +02:00
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
2024-08-13 17:02:11 +02:00
using Remora.Discord.API.Abstractions.Objects;
2024-08-13 13:08:50 +02:00
using Remora.Discord.API.Abstractions.Rest;
2024-08-24 19:02:19 +02:00
using Remora.Discord.Commands.Extensions;
2024-08-13 13:08:50 +02:00
using Remora.Discord.Commands.Feedback.Services;
2024-08-24 19:02:19 +02:00
using Remora.Discord.Commands.Services;
2024-08-13 16:48:54 +02:00
using Remora.Discord.Extensions.Embeds;
2024-08-13 13:08:50 +02:00
using Remora.Results;
using IClock = NodaTime.IClock;
2024-08-13 13:08:50 +02:00
using IResult = Remora.Results.IResult;
namespace Catalogger.Backend.Bot.Commands;
[Group("catalogger")]
2024-10-14 21:28:34 +02:00
[Description("Commands for information about the bot itself.")]
2024-08-13 13:08:50 +02:00
public class MetaCommands(
ILogger logger,
2024-08-13 13:08:50 +02:00
IClock clock,
Config config,
2024-08-24 19:02:19 +02:00
ShardedGatewayClient client,
IFeedbackService feedbackService,
2024-08-24 19:02:19 +02:00
ContextInjectionService contextInjection,
GuildCache guildCache,
2024-10-16 14:53:30 +02:00
RoleCache roleCache,
ChannelCache channelCache,
2024-10-14 17:09:12 +02:00
EmojiCache emojiCache,
2024-10-09 17:35:11 +02:00
IDiscordRestChannelAPI channelApi
) : CommandGroup
2024-08-13 13:08:50 +02:00
{
private readonly ILogger _logger = logger.ForContext<MetaCommands>();
private readonly HttpClient _client = new();
2024-08-13 13:08:50 +02:00
[Command("ping")]
[Description("Ping pong! See the bot's latency")]
public async Task<IResult> PingAsync()
{
2024-10-09 17:35:11 +02:00
var shardId =
contextInjection.Context?.TryGetGuildID(out var guildId) == true
? client.ShardIdFor(guildId.Value)
: 0;
2024-08-24 19:02:19 +02:00
2024-10-09 17:35:11 +02:00
var averageLatency =
client.Shards.Values.Select(x => x.Latency.TotalMilliseconds).Sum()
/ client.Shards.Count;
2024-08-24 19:02:19 +02:00
2024-08-13 13:08:50 +02:00
var t1 = clock.GetCurrentInstant();
var msg = await feedbackService.SendContextualAsync("...").GetOrThrow();
var elapsed = clock.GetCurrentInstant() - t1;
2024-08-13 16:48:54 +02:00
var process = Process.GetCurrentProcess();
var memoryUsage = process.WorkingSet64;
var embed = new EmbedBuilder()
.WithColour(DiscordUtils.Purple)
2024-10-09 17:35:11 +02:00
.WithFooter(
$"{RuntimeInformation.FrameworkDescription} on {RuntimeInformation.RuntimeIdentifier}"
)
2024-08-13 16:48:54 +02:00
.WithCurrentTimestamp();
2024-10-09 17:35:11 +02:00
embed.AddField(
"Ping",
$"Gateway: {client.Shards[shardId].Latency.TotalMilliseconds:N0}ms (average: {averageLatency:N0}ms)\n"
+ $"API: {elapsed.TotalMilliseconds:N0}ms",
inline: true
);
2024-08-13 17:02:11 +02:00
embed.AddField("Memory usage", memoryUsage.Bytes().Humanize(), inline: true);
2024-08-13 16:48:54 +02:00
var messageRate = await MessagesRate();
2024-10-09 17:35:11 +02:00
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",
2024-10-09 17:35:11 +02:00
true
);
2024-08-24 19:02:19 +02:00
embed.AddField("Shard", $"{shardId + 1} of {client.Shards.Count}", true);
2024-10-09 17:35:11 +02:00
embed.AddField(
"Uptime",
$"{(CataloggerMetrics.Startup - clock.GetCurrentInstant()).Prettify(TimeUnit.Second)}\n"
+ $"since <t:{CataloggerMetrics.Startup.ToUnixTimeSeconds()}:F>",
true
);
2024-08-13 16:48:54 +02:00
2024-10-09 17:35:11 +02:00
embed.AddField(
"Numbers",
$"{CataloggerMetrics.MessagesStored.Value:N0} messages "
2024-10-16 14:53:30 +02:00
+ $"from {guildCache.Size:N0} servers\n"
+ $"Cached {channelCache.Size:N0} channels, {roleCache.Size:N0} roles, {emojiCache.Size:N0} emojis",
2024-10-09 17:35:11 +02:00
false
);
2024-08-24 19:02:19 +02:00
IEmbed[] embeds = [embed.Build().GetOrThrow()];
2024-08-13 17:02:11 +02:00
2024-10-09 17:35:11 +02:00
return (Result)
await channelApi.EditMessageAsync(msg.ChannelID, msg.ID, content: "", embeds: embeds);
2024-08-13 13:08:50 +02:00
}
// TODO: add more checks around response format, configurable prometheus endpoint
private async Task<double?> MessagesRate()
{
2024-10-09 17:35:11 +02:00
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
2024-10-09 17:35:11 +02:00
}