diff --git a/PluralKit.Bot/Bot.cs b/PluralKit.Bot/Bot.cs index 2333edf4..b4a0cf74 100644 --- a/PluralKit.Bot/Bot.cs +++ b/PluralKit.Bot/Bot.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; +using App.Metrics; using Dapper; using Discord; using Discord.Commands; @@ -16,6 +17,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using NodaTime; using Npgsql; +using Npgsql.Logging; using Sentry; using Sentry.Extensibility; @@ -92,6 +94,8 @@ namespace PluralKit.Bot .AddTransient() .AddTransient() + .AddSingleton(_ => AppMetrics.CreateDefaultBuilder().Build()) + .BuildServiceProvider(); } class Bot @@ -101,13 +105,15 @@ namespace PluralKit.Bot private CommandService _commands; private ProxyService _proxy; private Timer _updateTimer; + private IMetrics _metrics; - public Bot(IServiceProvider services, IDiscordClient client, CommandService commands, ProxyService proxy) + public Bot(IServiceProvider services, IDiscordClient client, CommandService commands, ProxyService proxy, IMetrics metrics) { this._services = services; this._client = client as DiscordShardedClient; this._commands = commands; this._proxy = proxy; + _metrics = metrics; } public async Task Init() @@ -142,6 +148,8 @@ namespace PluralKit.Bot private async Task CommandExecuted(Optional cmd, ICommandContext ctx, IResult _result) { + _metrics.Measure.Meter.Mark(BotMetrics.CommandsRun); + // TODO: refactor this entire block, it's fugly. if (!_result.IsSuccess) { if (_result.Error == CommandError.Unsuccessful || _result.Error == CommandError.Exception) { @@ -168,6 +176,8 @@ namespace PluralKit.Bot private async Task MessageReceived(SocketMessage _arg) { + _metrics.Measure.Meter.Mark(BotMetrics.MessagesReceived); + // _client.CurrentUser will be null if we've connected *some* shards but not shard #0 yet // This will cause an error in WebhookCacheServices so we just workaround and don't process any messages // until we properly connect. TODO: can we do this without chucking away a bunch of messages? diff --git a/PluralKit.Bot/BotMetrics.cs b/PluralKit.Bot/BotMetrics.cs new file mode 100644 index 00000000..7bd65df1 --- /dev/null +++ b/PluralKit.Bot/BotMetrics.cs @@ -0,0 +1,16 @@ +using App.Metrics; +using App.Metrics.Gauge; +using App.Metrics.Meter; + +namespace PluralKit.Bot +{ + public static class BotMetrics + { + public static MeterOptions MessagesReceived => new MeterOptions {Name = "Messages processed", MeasurementUnit = Unit.Events, RateUnit = TimeUnit.Seconds, Context = "Bot"}; + public static MeterOptions MessagesProxied => new MeterOptions {Name = "Messages proxied", MeasurementUnit = Unit.Events, RateUnit = TimeUnit.Seconds, Context = "Bot"}; + public static MeterOptions CommandsRun => new MeterOptions {Name = "Commands run", MeasurementUnit = Unit.Commands, RateUnit = TimeUnit.Seconds, Context = "Bot"}; + public static GaugeOptions MembersOnline => new GaugeOptions {Name = "Members online", MeasurementUnit = Unit.None, Context = "Bot"}; + + public static GaugeOptions DatabasePoolSize => new GaugeOptions { Name = "Database pool size", Context = "Database" }; + } +} \ No newline at end of file diff --git a/PluralKit.Bot/Commands/MiscCommands.cs b/PluralKit.Bot/Commands/MiscCommands.cs index 34afa2f8..7b3ba7c9 100644 --- a/PluralKit.Bot/Commands/MiscCommands.cs +++ b/PluralKit.Bot/Commands/MiscCommands.cs @@ -1,10 +1,14 @@ +using System.Linq; using System.Threading.Tasks; +using App.Metrics; +using App.Metrics.Meter; using Discord; using Discord.Commands; namespace PluralKit.Bot.Commands { public class MiscCommands: ModuleBase { public BotConfig BotConfig { get; set; } + public IMetrics Metrics { get; set; } [Command("invite")] [Alias("inv")] @@ -32,5 +36,21 @@ namespace PluralKit.Bot.Commands { [Command("freeze")] public Task Freeze() => Context.Channel.SendMessageAsync("*A giant crystal ball of ice is charged and hurled toward your opponent, bursting open and freezing them solid on contact.*"); [Command("starstorm")] public Task Starstorm() => Context.Channel.SendMessageAsync("*Vibrant colours burst forth from the sky as meteors rain down upon your opponent.*"); + [Command("stats")] + public async Task Stats() + { + var messagesReceived = Metrics.Snapshot.GetForContext("Bot").Meters.First(m => m.MultidimensionalName == BotMetrics.MessagesReceived.Name).Value; + var messagesProxied = Metrics.Snapshot.GetForContext("Bot").Meters.First(m => m.MultidimensionalName == BotMetrics.MessagesProxied.Name).Value; + var proxySuccessRate = messagesProxied.Items.First(i => i.Item == "success"); + + var commandsRun = Metrics.Snapshot.GetForContext("Bot").Meters.First(m => m.MultidimensionalName == BotMetrics.CommandsRun.Name).Value; + + await Context.Channel.SendMessageAsync(embed: new EmbedBuilder() + .AddField("Messages processed", $"{messagesReceived.OneMinuteRate:F1}/s ({messagesReceived.FifteenMinuteRate:F1}/s over 15m)") + .AddField("Messages proxied", $"{messagesProxied.OneMinuteRate:F1}/s ({messagesProxied.FifteenMinuteRate:F1}/s over 15m)") + .AddField("Commands executed", $"{commandsRun.OneMinuteRate:F1}/s ({commandsRun.FifteenMinuteRate:F1}/s over 15m)") + .AddField("Proxy success rate", $"{proxySuccessRate.Percent/100:P1}") + .Build()); + } } } \ No newline at end of file diff --git a/PluralKit.Bot/Services/ProxyService.cs b/PluralKit.Bot/Services/ProxyService.cs index 89378ca3..779f14e6 100644 --- a/PluralKit.Bot/Services/ProxyService.cs +++ b/PluralKit.Bot/Services/ProxyService.cs @@ -6,6 +6,7 @@ using System.Net; using System.Net.Http; using System.Text.RegularExpressions; using System.Threading.Tasks; +using App.Metrics; using Dapper; using Discord; using Discord.Net; @@ -35,8 +36,9 @@ namespace PluralKit.Bot private WebhookCacheService _webhookCache; private MessageStore _messageStorage; private EmbedService _embeds; + private IMetrics _metrics; - public ProxyService(IDiscordClient client, WebhookCacheService webhookCache, DbConnectionFactory conn, LogChannelService logger, MessageStore messageStorage, EmbedService embeds) + public ProxyService(IDiscordClient client, WebhookCacheService webhookCache, DbConnectionFactory conn, LogChannelService logger, MessageStore messageStorage, EmbedService embeds, IMetrics metrics) { _client = client; _webhookCache = webhookCache; @@ -44,6 +46,7 @@ namespace PluralKit.Bot _logger = logger; _messageStorage = messageStorage; _embeds = embeds; + _metrics = metrics; } private ProxyMatch GetProxyTagMatch(string message, IEnumerable potentials) @@ -159,15 +162,33 @@ namespace PluralKit.Bot } ulong messageId; - if (attachment != null) { - using (var http = new HttpClient()) - using (var stream = await http.GetStreamAsync(attachment.Url)) { - messageId = await client.SendFileAsync(stream, filename: attachment.Filename, text: text, username: username, avatarUrl: avatarUrl); + + try + { + if (attachment != null) + { + using (var http = new HttpClient()) + using (var stream = await http.GetStreamAsync(attachment.Url)) + { + messageId = await client.SendFileAsync(stream, filename: attachment.Filename, text: text, + username: username, avatarUrl: avatarUrl); + } } - } else { - messageId = await client.SendMessageAsync(text, username: username, avatarUrl: avatarUrl); + else + { + messageId = await client.SendMessageAsync(text, username: username, avatarUrl: avatarUrl); + } + + // Log it in the metrics + _metrics.Measure.Meter.Mark(BotMetrics.MessagesProxied, "success"); } - + catch (HttpException) + { + // Log failure in metrics and rethrow (we still need to cancel everything else) + _metrics.Measure.Meter.Mark(BotMetrics.MessagesProxied, "failure"); + throw; + } + // TODO: figure out a way to return the full message object (without doing a GetMessageAsync call, which // doesn't work if there's no permission to) return messageId; diff --git a/PluralKit.Core/CoreMetrics.cs b/PluralKit.Core/CoreMetrics.cs new file mode 100644 index 00000000..8c7ae6ef --- /dev/null +++ b/PluralKit.Core/CoreMetrics.cs @@ -0,0 +1,13 @@ +using App.Metrics; +using App.Metrics.Gauge; + +namespace PluralKit.Core +{ + public static class CoreMetrics + { + public static GaugeOptions SystemCount => new GaugeOptions { Name = "Systems", MeasurementUnit = Unit.Items}; + public static GaugeOptions MemberCount => new GaugeOptions { Name = "Members", MeasurementUnit = Unit.Items }; + public static GaugeOptions MessageCount => new GaugeOptions { Name = "Messages", MeasurementUnit = Unit.Items }; + public static GaugeOptions SwitchCount => new GaugeOptions { Name = "Switches", MeasurementUnit = Unit.Items }; + } +} \ No newline at end of file diff --git a/PluralKit.Core/PluralKit.Core.csproj b/PluralKit.Core/PluralKit.Core.csproj index 0fd4f277..71d3d2ac 100644 --- a/PluralKit.Core/PluralKit.Core.csproj +++ b/PluralKit.Core/PluralKit.Core.csproj @@ -5,6 +5,7 @@ +