Add basic logging framework

This commit is contained in:
Ske
2019-07-18 17:13:42 +02:00
parent dfbb5cd2d6
commit 961bfe9094
12 changed files with 181 additions and 63 deletions

View File

@@ -11,7 +11,12 @@ using Discord.Commands;
using Discord.WebSocket;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using NodaTime;
using Sentry;
using Serilog;
using Serilog.Core;
using Serilog.Formatting.Compact;
using Serilog.Sinks.SystemConsole.Themes;
namespace PluralKit.Bot
{
@@ -26,6 +31,15 @@ namespace PluralKit.Bot
Console.WriteLine("Starting PluralKit...");
InitUtils.Init();
// Set up a CancellationToken and a SIGINT hook to properly dispose of things when the app is closed
// The Task.Delay line will throw/exit (forgot which) and the stack and using statements will properly unwind
var token = new CancellationTokenSource();
Console.CancelKeyPress += delegate(object e, ConsoleCancelEventArgs args)
{
args.Cancel = true;
token.Cancel();
};
using (var services = BuildServiceProvider())
{
@@ -42,59 +56,76 @@ namespace PluralKit.Bot
Console.WriteLine("- Connecting to Discord...");
var client = services.GetRequiredService<IDiscordClient>() as DiscordShardedClient;
await client.LoginAsync(TokenType.Bot, botConfig.Token);
await client.StartAsync();
Console.WriteLine("- Initializing bot...");
await services.GetRequiredService<Bot>().Init();
await client.StartAsync();
await Task.Delay(-1);
try
{
await Task.Delay(-1, token.Token);
}
catch (TaskCanceledException) { } // We'll just exit normally
Console.WriteLine("- Shutting down...");
}
}
}
public ServiceProvider BuildServiceProvider() => new ServiceCollection()
.AddTransient(_ => _config.GetSection("PluralKit").Get<CoreConfig>() ?? new CoreConfig())
.AddTransient(_ => _config.GetSection("PluralKit").GetSection("Bot").Get<BotConfig>() ?? new BotConfig())
.AddTransient(svc => new DbConnectionFactory(svc.GetRequiredService<CoreConfig>().Database))
.AddSingleton<IDiscordClient, DiscordShardedClient>()
.AddSingleton<Bot>()
.AddTransient(_ => _config.GetSection("PluralKit").Get<CoreConfig>() ?? new CoreConfig())
.AddTransient(_ => _config.GetSection("PluralKit").GetSection("Bot").Get<BotConfig>() ?? new BotConfig())
.AddTransient<CommandService>(_ => new CommandService(new CommandServiceConfig
.AddTransient(svc => new DbConnectionFactory(svc.GetRequiredService<CoreConfig>().Database))
.AddSingleton<IDiscordClient, DiscordShardedClient>()
.AddSingleton<Bot>()
.AddTransient<CommandService>(_ => new CommandService(new CommandServiceConfig
{
CaseSensitiveCommands = false,
QuotationMarkAliasMap = new Dictionary<char, char>
{
CaseSensitiveCommands = false,
QuotationMarkAliasMap = new Dictionary<char, char>
{
{'"', '"'},
{'\'', '\''},
{'', ''},
{'“', '”'},
{'„', '‟'},
},
DefaultRunMode = RunMode.Async
}))
.AddTransient<EmbedService>()
.AddTransient<ProxyService>()
.AddTransient<LogChannelService>()
.AddTransient<DataFileService>()
.AddSingleton<WebhookCacheService>()
.AddTransient<SystemStore>()
.AddTransient<MemberStore>()
.AddTransient<MessageStore>()
.AddTransient<SwitchStore>()
.AddSingleton<IMetrics>(svc =>
{
var cfg = svc.GetRequiredService<CoreConfig>();
var builder = AppMetrics.CreateDefaultBuilder();
if (cfg.InfluxUrl != null && cfg.InfluxDb != null)
builder.Report.ToInfluxDb(cfg.InfluxUrl, cfg.InfluxDb);
return builder.Build();
})
.AddSingleton<PeriodicStatCollector>()
{'"', '"'},
{'\'', '\''},
{'', ''},
{'', ''},
{'', ''},
},
DefaultRunMode = RunMode.Async
}))
.AddTransient<EmbedService>()
.AddTransient<ProxyService>()
.AddTransient<LogChannelService>()
.AddTransient<DataFileService>()
.AddSingleton<WebhookCacheService>()
.AddTransient<SystemStore>()
.AddTransient<MemberStore>()
.AddTransient<MessageStore>()
.AddTransient<SwitchStore>()
.AddSingleton<IMetrics>(svc =>
{
var cfg = svc.GetRequiredService<CoreConfig>();
var builder = AppMetrics.CreateDefaultBuilder();
if (cfg.InfluxUrl != null && cfg.InfluxDb != null)
builder.Report.ToInfluxDb(cfg.InfluxUrl, cfg.InfluxDb);
return builder.Build();
})
.AddSingleton<PeriodicStatCollector>()
.AddSingleton<ILogger>(svc => new LoggerConfiguration()
.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb)
.WriteTo.File(
new CompactJsonFormatter(),
(svc.GetRequiredService<CoreConfig>().LogDir ?? "logs") + "/pluralkit.bot.log",
rollingInterval: RollingInterval.Day,
flushToDiskInterval: TimeSpan.FromSeconds(10),
buffered: true)
.WriteTo.Console(theme: AnsiConsoleTheme.Code)
.CreateLogger())
.BuildServiceProvider();
}
@@ -107,8 +138,9 @@ namespace PluralKit.Bot
private Timer _updateTimer;
private IMetrics _metrics;
private PeriodicStatCollector _collector;
private ILogger _logger;
public Bot(IServiceProvider services, IDiscordClient client, CommandService commands, ProxyService proxy, IMetrics metrics, PeriodicStatCollector collector)
public Bot(IServiceProvider services, IDiscordClient client, CommandService commands, ProxyService proxy, IMetrics metrics, PeriodicStatCollector collector, ILogger logger)
{
this._services = services;
this._client = client as DiscordShardedClient;
@@ -116,6 +148,7 @@ namespace PluralKit.Bot
this._proxy = proxy;
_metrics = metrics;
_collector = collector;
_logger = logger.ForContext<Bot>();
}
public async Task Init()
@@ -141,12 +174,14 @@ namespace PluralKit.Bot
await _client.SetGameAsync($"pk;help | in {_client.Guilds.Count} servers");
await _collector.CollectStats();
_logger.Information("Submitted metrics to backend");
await Task.WhenAll(((IMetricsRoot) _metrics).ReportRunner.RunAllAsync());
}
private async Task ShardReady(DiscordSocketClient shardClient)
{
_logger.Information("Shard {Shard} connected", shardClient.ShardId);
Console.WriteLine($"Shard #{shardClient.ShardId} connected to {shardClient.Guilds.Sum(g => g.Channels.Count)} channels in {shardClient.Guilds.Count} guilds.");
if (shardClient.ShardId == 0)
@@ -207,7 +242,7 @@ namespace PluralKit.Bot
// Ignore system messages (member joined, message pinned, etc)
var arg = _arg as SocketUserMessage;
if (arg == null) return;
// Ignore bot messages
if (arg.Author.IsBot || arg.Author.IsWebhook) return;
@@ -243,6 +278,7 @@ namespace PluralKit.Bot
private void HandleRuntimeError(Exception e)
{
_logger.Error(e, "Exception in bot event handler");
SentrySdk.CaptureException(e);
Console.Error.WriteLine(e);
}

View File

@@ -15,6 +15,10 @@
<PackageReference Include="Discord.Net.WebSocket" Version="2.0.1" />
<PackageReference Include="Humanizer.Core" Version="2.6.2" />
<PackageReference Include="Sentry" Version="2.0.0-beta2" />
<PackageReference Include="Serilog.Formatting.Compact" Version="1.0.0" />
<PackageReference Include="Serilog.NodaTime" Version="1.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-beta0006" />
</ItemGroup>

View File

@@ -1,6 +1,7 @@
using System.Threading.Tasks;
using Dapper;
using Discord;
using Serilog;
namespace PluralKit.Bot {
public class ServerDefinition {
@@ -12,12 +13,14 @@ namespace PluralKit.Bot {
private IDiscordClient _client;
private DbConnectionFactory _conn;
private EmbedService _embed;
private ILogger _logger;
public LogChannelService(IDiscordClient client, DbConnectionFactory conn, EmbedService embed)
public LogChannelService(IDiscordClient client, DbConnectionFactory conn, EmbedService embed, ILogger logger)
{
this._client = client;
this._conn = conn;
this._embed = embed;
_logger = logger.ForContext<LogChannelService>();
}
public async Task LogMessage(PKSystem system, PKMember member, ulong messageId, IGuildChannel originalChannel, IUser sender, string content) {
@@ -53,6 +56,8 @@ namespace PluralKit.Bot {
"insert into servers (id, log_channel) values (@Id, @LogChannel) on conflict (id) do update set log_channel = @LogChannel",
def);
}
_logger.Information("Set guild {} log channel to {Channel}", guild.Id, newLogChannel?.Id);
}
}
}

View File

@@ -1,10 +1,13 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using App.Metrics;
using Discord;
using Discord.WebSocket;
using NodaTime.Extensions;
using PluralKit.Core;
using Serilog;
namespace PluralKit.Bot
{
@@ -18,7 +21,9 @@ namespace PluralKit.Bot
private SwitchStore _switches;
private MessageStore _messages;
public PeriodicStatCollector(IDiscordClient client, IMetrics metrics, SystemStore systems, MemberStore members, SwitchStore switches, MessageStore messages)
private ILogger _logger;
public PeriodicStatCollector(IDiscordClient client, IMetrics metrics, SystemStore systems, MemberStore members, SwitchStore switches, MessageStore messages, ILogger logger)
{
_client = (DiscordShardedClient) client;
_metrics = metrics;
@@ -26,10 +31,14 @@ namespace PluralKit.Bot
_members = members;
_switches = switches;
_messages = messages;
_logger = logger.ForContext<PeriodicStatCollector>();
}
public async Task CollectStats()
{
var stopwatch = new Stopwatch();
stopwatch.Start();
// 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));
@@ -52,6 +61,9 @@ namespace PluralKit.Bot
_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());
stopwatch.Stop();
_logger.Information("Updated metrics in {Time}", stopwatch.ElapsedDuration());
}
}
}

View File

@@ -10,6 +10,7 @@ using Discord;
using Discord.Net;
using Discord.Webhook;
using Discord.WebSocket;
using Serilog;
namespace PluralKit.Bot
{
@@ -30,21 +31,23 @@ namespace PluralKit.Bot
class ProxyService {
private IDiscordClient _client;
private DbConnectionFactory _conn;
private LogChannelService _logger;
private LogChannelService _logChannel;
private WebhookCacheService _webhookCache;
private MessageStore _messageStorage;
private EmbedService _embeds;
private IMetrics _metrics;
private ILogger _logger;
public ProxyService(IDiscordClient client, WebhookCacheService webhookCache, DbConnectionFactory conn, LogChannelService logger, MessageStore messageStorage, EmbedService embeds, IMetrics metrics)
public ProxyService(IDiscordClient client, WebhookCacheService webhookCache, DbConnectionFactory conn, LogChannelService logChannel, MessageStore messageStorage, EmbedService embeds, IMetrics metrics, ILogger logger)
{
_client = client;
_webhookCache = webhookCache;
_conn = conn;
_logger = logger;
_logChannel = logChannel;
_messageStorage = messageStorage;
_embeds = embeds;
_metrics = metrics;
_logger = logger.ForContext<ProxyService>();
}
private ProxyMatch GetProxyTagMatch(string message, IEnumerable<ProxyDatabaseResult> potentials)
@@ -108,7 +111,7 @@ namespace PluralKit.Bot
// Store the message in the database, and log it in the log channel (if applicable)
await _messageStorage.Store(message.Author.Id, hookMessageId, message.Channel.Id, match.Member);
await _logger.LogMessage(match.System, match.Member, hookMessageId, message.Channel as IGuildChannel, message.Author, match.InnerText);
await _logChannel.LogMessage(match.System, match.Member, hookMessageId, message.Channel as IGuildChannel, message.Author, match.InnerText);
// Wait a second or so before deleting the original message
await Task.Delay(1000);
@@ -177,11 +180,15 @@ namespace PluralKit.Bot
messageId = await client.SendMessageAsync(text, username: username, avatarUrl: avatarUrl);
}
_logger.Information("Invoked webhook {Webhook} in channel {Channel}", webhook.Id, webhook.Channel);
// Log it in the metrics
_metrics.Measure.Meter.Mark(BotMetrics.MessagesProxied, "success");
}
catch (HttpException)
catch (HttpException e)
{
_logger.Warning(e, "Error invoking webhook {Webhook} in channel {Channel}", webhook.Id, webhook.ChannelId);
// Log failure in metrics and rethrow (we still need to cancel everything else)
_metrics.Measure.Meter.Mark(BotMetrics.MessagesProxied, "failure");
throw;