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.Extensions; using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Commands.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, ShardedGatewayClient client, IFeedbackService feedbackService, ContextInjectionService contextInjection, GuildCache guildCache, ChannelCache channelCache, IDiscordRestChannelAPI channelApi ) : CommandGroup { private readonly ILogger _logger = logger.ForContext(); private readonly HttpClient _client = new(); [Command("ping")] [Description("Ping pong! See the bot's latency")] public async Task PingAsync() { var shardId = contextInjection.Context?.TryGetGuildID(out var guildId) == true ? client.ShardIdFor(guildId.Value) : 0; var averageLatency = client.Shards.Values.Select(x => x.Latency.TotalMilliseconds).Sum() / client.Shards.Count; 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.Shards[shardId].Latency.TotalMilliseconds:N0}ms (average: {averageLatency:N0}ms)\n" + $"API: {elapsed.TotalMilliseconds:N0}ms", 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("Shard", $"{shardId + 1} of {client.Shards.Count}", true); embed.AddField( "Uptime", $"{(CataloggerMetrics.Startup - clock.GetCurrentInstant()).Prettify(TimeUnit.Second)}\n" + $"since ", true ); embed.AddField( "Numbers", $"{CataloggerMetrics.MessagesStored.Value:N0} messages " + $"from {guildCache.Size:N0} servers\nCached {channelCache.Size:N0} channels", false ); 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 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(); 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 }