diff --git a/PluralKit.Bot/Bot.cs b/PluralKit.Bot/Bot.cs index b4a0cf74..5daa3ed7 100644 --- a/PluralKit.Bot/Bot.cs +++ b/PluralKit.Bot/Bot.cs @@ -18,6 +18,7 @@ using Microsoft.Extensions.DependencyInjection; using NodaTime; using Npgsql; using Npgsql.Logging; +using PluralKit.Core; using Sentry; using Sentry.Extensibility; @@ -94,7 +95,15 @@ namespace PluralKit.Bot .AddTransient() .AddTransient() - .AddSingleton(_ => AppMetrics.CreateDefaultBuilder().Build()) + .AddSingleton(svc => + { + var cfg = svc.GetRequiredService(); + var builder = AppMetrics.CreateDefaultBuilder(); + if (cfg.InfluxUrl != null && cfg.InfluxDb != null) + builder.Report.ToInfluxDb(cfg.InfluxUrl, cfg.InfluxDb); + return builder.Build(); + }) + .AddSingleton() .BuildServiceProvider(); } @@ -106,14 +115,16 @@ namespace PluralKit.Bot private ProxyService _proxy; private Timer _updateTimer; private IMetrics _metrics; + private PeriodicStatCollector _collector; - public Bot(IServiceProvider services, IDiscordClient client, CommandService commands, ProxyService proxy, IMetrics metrics) + public Bot(IServiceProvider services, IDiscordClient client, CommandService commands, ProxyService proxy, IMetrics metrics, PeriodicStatCollector collector) { this._services = services; this._client = client as DiscordShardedClient; this._commands = commands; this._proxy = proxy; _metrics = metrics; + _collector = collector; } public async Task Init() @@ -132,18 +143,28 @@ namespace PluralKit.Bot _client.MessageDeleted += async (message, channel) => _proxy.HandleMessageDeletedAsync(message, channel).CatchException(HandleRuntimeError); } + // Method called every 60 seconds private async Task UpdatePeriodic() { - // Method called every 60 seconds + // Change bot status await _client.SetGameAsync($"pk;help | in {_client.Guilds.Count} servers"); + + await _collector.CollectStats(); + await Task.WhenAll(((IMetricsRoot) _metrics).ReportRunner.RunAllAsync()); } private async Task ShardReady(DiscordSocketClient shardClient) { - //_updateTimer = new Timer((_) => UpdatePeriodic(), null, 0, 60*1000); Console.WriteLine($"Shard #{shardClient.ShardId} connected to {shardClient.Guilds.Sum(g => g.Channels.Count)} channels in {shardClient.Guilds.Count} guilds."); - //Console.WriteLine($"PluralKit started as {_client.CurrentUser.Username}#{_client.CurrentUser.Discriminator} ({_client.CurrentUser.Id})."); + + if (shardClient.ShardId == 0) + { + _updateTimer = new Timer((_) => UpdatePeriodic().CatchException(HandleRuntimeError), null, 0, 60*1000); + + Console.WriteLine( + $"PluralKit started as {_client.CurrentUser.Username}#{_client.CurrentUser.Discriminator} ({_client.CurrentUser.Id})."); + } } private async Task CommandExecuted(Optional cmd, ICommandContext ctx, IResult _result) diff --git a/PluralKit.Bot/BotMetrics.cs b/PluralKit.Bot/BotMetrics.cs index 7bd65df1..baafabf3 100644 --- a/PluralKit.Bot/BotMetrics.cs +++ b/PluralKit.Bot/BotMetrics.cs @@ -9,7 +9,10 @@ namespace PluralKit.Bot 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 MembersTotal => new GaugeOptions {Name = "Members total", MeasurementUnit = Unit.None, Context = "Bot"}; public static GaugeOptions MembersOnline => new GaugeOptions {Name = "Members online", MeasurementUnit = Unit.None, Context = "Bot"}; + public static GaugeOptions Guilds => new GaugeOptions {Name = "Guilds", MeasurementUnit = Unit.None, Context = "Bot"}; + public static GaugeOptions Channels => new GaugeOptions {Name = "Channels", MeasurementUnit = Unit.None, Context = "Bot"}; public static GaugeOptions DatabasePoolSize => new GaugeOptions { Name = "Database pool size", Context = "Database" }; } diff --git a/PluralKit.Bot/Services/PeriodicStatCollector.cs b/PluralKit.Bot/Services/PeriodicStatCollector.cs new file mode 100644 index 00000000..8d81a74f --- /dev/null +++ b/PluralKit.Bot/Services/PeriodicStatCollector.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using App.Metrics; +using Discord; +using Discord.WebSocket; +using PluralKit.Core; + +namespace PluralKit.Bot +{ + public class PeriodicStatCollector + { + private DiscordShardedClient _client; + private IMetrics _metrics; + + private SystemStore _systems; + private MemberStore _members; + private SwitchStore _switches; + private MessageStore _messages; + + public PeriodicStatCollector(IDiscordClient client, IMetrics metrics, SystemStore systems, MemberStore members, SwitchStore switches, MessageStore messages) + { + _client = (DiscordShardedClient) client; + _metrics = metrics; + _systems = systems; + _members = members; + _switches = switches; + _messages = messages; + } + + public async Task CollectStats() + { + // Aggregate guild/channel stats + _metrics.Measure.Gauge.SetValue(BotMetrics.Guilds, _client.Guilds.Count); + _metrics.Measure.Gauge.SetValue(BotMetrics.Channels, _client.Guilds.Sum(g => g.TextChannels.Count)); + + // Aggregate member stats + var usersKnown = new HashSet(); + var usersOnline = new HashSet(); + foreach (var guild in _client.Guilds) + foreach (var user in guild.Users) + { + usersKnown.Add(user.Id); + if (user.Status == UserStatus.Online) usersOnline.Add(user.Id); + } + + _metrics.Measure.Gauge.SetValue(BotMetrics.MembersTotal, usersKnown.Count); + _metrics.Measure.Gauge.SetValue(BotMetrics.MembersOnline, usersOnline.Count); + + // Aggregate DB stats + _metrics.Measure.Gauge.SetValue(CoreMetrics.SystemCount, await _systems.Count()); + _metrics.Measure.Gauge.SetValue(CoreMetrics.MemberCount, await _members.Count()); + _metrics.Measure.Gauge.SetValue(CoreMetrics.SwitchCount, await _switches.Count()); + _metrics.Measure.Gauge.SetValue(CoreMetrics.MessageCount, await _messages.Count()); + } + } +} \ No newline at end of file diff --git a/PluralKit.Core/CoreConfig.cs b/PluralKit.Core/CoreConfig.cs index 12286226..9c5d5df8 100644 --- a/PluralKit.Core/CoreConfig.cs +++ b/PluralKit.Core/CoreConfig.cs @@ -4,5 +4,7 @@ namespace PluralKit { public string Database { get; set; } public string SentryUrl { get; set; } + public string InfluxUrl { get; set; } + public string InfluxDb { get; set; } } } \ No newline at end of file diff --git a/PluralKit.Core/PluralKit.Core.csproj b/PluralKit.Core/PluralKit.Core.csproj index 71d3d2ac..7f82a50e 100644 --- a/PluralKit.Core/PluralKit.Core.csproj +++ b/PluralKit.Core/PluralKit.Core.csproj @@ -6,6 +6,7 @@ + diff --git a/PluralKit.Core/Stores.cs b/PluralKit.Core/Stores.cs index 7433ecd8..ce909066 100644 --- a/PluralKit.Core/Stores.cs +++ b/PluralKit.Core/Stores.cs @@ -72,6 +72,12 @@ namespace PluralKit { using (var conn = await _conn.Obtain()) return await conn.QueryAsync("select uid from accounts where system = @Id", new { Id = system.Id }); } + + public async Task Count() + { + using (var conn = await _conn.Obtain()) + return await conn.ExecuteScalarAsync("select count(id) from systems"); + } } public class MemberStore { @@ -138,6 +144,12 @@ namespace PluralKit { using (var conn = await _conn.Obtain()) return await conn.ExecuteScalarAsync("select count(*) from members where system = @Id", system); } + + public async Task Count() + { + using (var conn = await _conn.Obtain()) + return await conn.ExecuteScalarAsync("select count(id) from members"); + } } public class MessageStore { @@ -185,6 +197,12 @@ namespace PluralKit { using (var conn = await _conn.Obtain()) await conn.ExecuteAsync("delete from messages where mid = @Id", new { Id = id }); } + + public async Task Count() + { + using (var conn = await _conn.Obtain()) + return await conn.ExecuteScalarAsync("select count(mid) from messages"); + } } public class SwitchStore @@ -258,6 +276,12 @@ namespace PluralKit { await conn.ExecuteAsync("delete from switches where id = @Id", new {Id = sw.Id}); } + public async Task Count() + { + using (var conn = await _conn.Obtain()) + return await conn.ExecuteScalarAsync("select count(id) from switches"); + } + public struct SwitchListEntry { public ICollection Members; diff --git a/docker-compose.yml b/docker-compose.yml index f3d55f5f..5847def8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,10 +5,13 @@ services: entrypoint: ["dotnet", "run", "--project", "PluralKit.Bot"] environment: - "PluralKit:Database=Host=db;Username=postgres;Password=postgres;Database=postgres" + - "PluralKit:InfluxUrl=http://influx:8086" + - "PluralKit:InfluxDb=pluralkit volumes: - "./pluralkit.conf:/app/pluralkit.conf:ro" links: - db + - influx restart: always web: build: . @@ -35,6 +38,14 @@ services: volumes: - "db_data:/var/lib/postgresql/data" restart: always + influx: + image: influxdb:alpine + volumes: + - "influx_data:/var/lib/influxdb" + ports: + - 2839:8086 + restart: always volumes: - db_data: \ No newline at end of file + db_data: + influx_data: \ No newline at end of file