diff --git a/PluralKit.API/Modules.cs b/PluralKit.API/Modules.cs new file mode 100644 index 00000000..4577f5a4 --- /dev/null +++ b/PluralKit.API/Modules.cs @@ -0,0 +1,12 @@ +using Autofac; + +namespace PluralKit.API +{ + public class APIModule: Module + { + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType().AsSelf(); + } + } +} \ No newline at end of file diff --git a/PluralKit.API/Program.cs b/PluralKit.API/Program.cs index 9108fd1b..674724bc 100644 --- a/PluralKit.API/Program.cs +++ b/PluralKit.API/Program.cs @@ -1,5 +1,8 @@ -using Microsoft.AspNetCore; +using Autofac.Extensions.DependencyInjection; + +using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; namespace PluralKit.API { @@ -8,13 +11,16 @@ namespace PluralKit.API public static void Main(string[] args) { InitUtils.Init(); - CreateWebHostBuilder(args).Build().Run(); + CreateHostBuilder(args).Build().Run(); } - public static IWebHostBuilder CreateWebHostBuilder(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseConfiguration(InitUtils.BuildConfiguration(args).Build()) - .ConfigureKestrel(opts => { opts.ListenAnyIP(5000);}) - .UseStartup(); + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseServiceProviderFactory(new AutofacServiceProviderFactory()) + .ConfigureWebHostDefaults(whb => whb + + .UseConfiguration(InitUtils.BuildConfiguration(args).Build()) + .ConfigureKestrel(opts => { opts.ListenAnyIP(5000); }) + .UseStartup()); } } \ No newline at end of file diff --git a/PluralKit.API/Startup.cs b/PluralKit.API/Startup.cs index e007dc8e..503d4487 100644 --- a/PluralKit.API/Startup.cs +++ b/PluralKit.API/Startup.cs @@ -1,10 +1,14 @@ -using Microsoft.AspNetCore.Builder; +using Autofac; + +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using PluralKit.Core; + namespace PluralKit.API { public class Startup @@ -23,20 +27,16 @@ namespace PluralKit.API services.AddControllers() .SetCompatibilityVersion(CompatibilityVersion.Latest) .AddNewtonsoftJson(); // sorry MS, this just does *more* + } - services - .AddTransient() - .AddSingleton() - - .AddSingleton(svc => InitUtils.InitMetrics(svc.GetRequiredService(), "API")) - - .AddScoped() - - .AddTransient(_ => Configuration.GetSection("PluralKit").Get() ?? new CoreConfig()) - .AddSingleton(svc => InitUtils.InitLogger(svc.GetRequiredService(), "api")) - - .AddTransient() - .AddTransient(); + public void ConfigureContainer(ContainerBuilder builder) + { + builder.RegisterInstance(Configuration); + builder.RegisterModule(new ConfigModule()); + builder.RegisterModule(new LoggingModule("api")); + builder.RegisterModule(new MetricsModule("API")); + builder.RegisterModule(); + builder.RegisterModule(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/PluralKit.Bot/Bot.cs b/PluralKit.Bot/Bot.cs index 353bcabf..083f51e1 100644 --- a/PluralKit.Bot/Bot.cs +++ b/PluralKit.Bot/Bot.cs @@ -7,6 +7,9 @@ using System.Threading; using System.Threading.Tasks; using App.Metrics; +using Autofac; +using Autofac.Core; + using Dapper; using Discord; using Discord.WebSocket; @@ -15,11 +18,16 @@ using Microsoft.Extensions.DependencyInjection; using PluralKit.Bot.Commands; using PluralKit.Bot.CommandSystem; +using PluralKit.Core; using Sentry; +using Sentry.Infrastructure; + using Serilog; using Serilog.Events; +using SystemClock = NodaTime.SystemClock; + namespace PluralKit.Bot { class Initialize @@ -45,105 +53,57 @@ namespace PluralKit.Bot args.Cancel = true; token.Cancel(); }; + + var builder = new ContainerBuilder(); + builder.RegisterInstance(_config); + builder.RegisterModule(new ConfigModule("Bot")); + builder.RegisterModule(new LoggingModule("bot")); + builder.RegisterModule(new MetricsModule()); + builder.RegisterModule(); + builder.RegisterModule(); + + using var services = builder.Build(); - using (var services = BuildServiceProvider()) + var logger = services.Resolve().ForContext(); + + try { SchemaService.Initialize(); + + var coreConfig = services.Resolve(); + var botConfig = services.Resolve(); + var schema = services.Resolve(); + + using var _ = Sentry.SentrySdk.Init(coreConfig.SentryUrl); - var logger = services.GetRequiredService().ForContext(); - var coreConfig = services.GetRequiredService(); - var botConfig = services.GetRequiredService(); - var schema = services.GetRequiredService(); + logger.Information("Connecting to database"); + await schema.ApplyMigrations(); - using (Sentry.SentrySdk.Init(coreConfig.SentryUrl)) + logger.Information("Connecting to Discord"); + var client = services.Resolve(); + await client.LoginAsync(TokenType.Bot, botConfig.Token); + + logger.Information("Initializing bot"); + await client.StartAsync(); + await services.Resolve().Init(); + + try { - logger.Information("Connecting to database"); - await schema.ApplyMigrations(); - - logger.Information("Connecting to Discord"); - var client = services.GetRequiredService() as DiscordShardedClient; - await client.LoginAsync(TokenType.Bot, botConfig.Token); - - logger.Information("Initializing bot"); - await services.GetRequiredService().Init(); - - await client.StartAsync(); - - try - { - await Task.Delay(-1, token.Token); - } - catch (TaskCanceledException) { } // We'll just exit normally - logger.Information("Shutting down"); + await Task.Delay(-1, token.Token); } + catch (TaskCanceledException) { } // We'll just exit normally } - } - - public ServiceProvider BuildServiceProvider() => new ServiceCollection() - .AddTransient(_ => _config.GetSection("PluralKit").Get() ?? new CoreConfig()) - .AddTransient(_ => _config.GetSection("PluralKit").GetSection("Bot").Get() ?? new BotConfig()) - - .AddSingleton() - .AddTransient() - .AddTransient() - - .AddSingleton(_ => new DiscordShardedClient(new DiscordSocketConfig + catch (Exception e) { - MessageCacheSize = 0, - ConnectionTimeout = 2*60*1000, - ExclusiveBulkDelete = true, - LargeThreshold = 50, - DefaultRetryMode = RetryMode.RetryTimeouts | RetryMode.RetryRatelimit - // Commented this out since Debug actually sends, uh, quite a lot that's not necessary in production - // but leaving it here in case I (or someone else) get[s] confused about why logging isn't working again :p - // LogLevel = LogSeverity.Debug // We filter log levels in Serilog, so just pass everything through (Debug is lower than Verbose) - })) - .AddSingleton() - .AddSingleton(_ => new HttpClient { Timeout = TimeSpan.FromSeconds(5) }) - .AddTransient() - - .AddTransient() - .AddTransient() - .AddTransient() - .AddTransient() - .AddTransient() - .AddTransient() - .AddTransient() - .AddTransient() - .AddTransient() - .AddTransient() - - .AddTransient() - .AddTransient() - .AddTransient() - .AddTransient() - .AddTransient() + logger.Fatal(e, "Unrecoverable error while initializing bot"); + } - .AddTransient() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - - .AddTransient() - - .AddSingleton(svc => InitUtils.InitMetrics(svc.GetRequiredService())) - .AddSingleton() - - .AddScoped(_ => new Sentry.Scope(null)) - .AddTransient() - - .AddScoped() - .AddSingleton(svc => new LoggerProvider(svc.GetRequiredService(), "bot")) - .AddScoped(svc => svc.GetRequiredService().RootLogger.ForContext("EventId", svc.GetRequiredService().EventId)) - - .AddMemoryCache() - - .BuildServiceProvider(); + logger.Information("Shutting down"); + } } class Bot { - private IServiceProvider _services; + private ILifetimeScope _services; private DiscordShardedClient _client; private Timer _updateTimer; private IMetrics _metrics; @@ -151,7 +111,7 @@ namespace PluralKit.Bot private ILogger _logger; private PKPerformanceEventListener _pl; - public Bot(IServiceProvider services, IDiscordClient client, IMetrics metrics, PeriodicStatCollector collector, ILogger logger) + public Bot(ILifetimeScope services, IDiscordClient client, IMetrics metrics, PeriodicStatCollector collector, ILogger logger) { _pl = new PKPerformanceEventListener(); _services = services; @@ -167,12 +127,12 @@ namespace PluralKit.Bot _client.ShardReady += ShardReady; _client.Log += FrameworkLog; - _client.MessageReceived += (msg) => HandleEvent(s => s.AddMessageBreadcrumb(msg), eh => eh.HandleMessage(msg)); - _client.ReactionAdded += (msg, channel, reaction) => HandleEvent(s => s.AddReactionAddedBreadcrumb(msg, channel, reaction), eh => eh.HandleReactionAdded(msg, channel, reaction)); - _client.MessageDeleted += (msg, channel) => HandleEvent(s => s.AddMessageDeleteBreadcrumb(msg, channel), eh => eh.HandleMessageDeleted(msg, channel)); - _client.MessagesBulkDeleted += (msgs, channel) => HandleEvent(s => s.AddMessageBulkDeleteBreadcrumb(msgs, channel), eh => eh.HandleMessagesBulkDelete(msgs, channel)); + _client.MessageReceived += (msg) => HandleEvent(eh => eh.HandleMessage(msg)); + _client.ReactionAdded += (msg, channel, reaction) => HandleEvent(eh => eh.HandleReactionAdded(msg, channel, reaction)); + _client.MessageDeleted += (msg, channel) => HandleEvent(eh => eh.HandleMessageDeleted(msg, channel)); + _client.MessagesBulkDeleted += (msgs, channel) => HandleEvent(eh => eh.HandleMessagesBulkDelete(msgs, channel)); - _services.GetService().Init(_client); + _services.Resolve().Init(_client); return Task.CompletedTask; } @@ -218,24 +178,24 @@ namespace PluralKit.Bot private 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."); + _logger.Information("Shard {Shard} connected to {ChannelCount} channels in {GuildCount} guilds", shardClient.ShardId, shardClient.Guilds.Sum(g => g.Channels.Count), shardClient.Guilds.Count); if (shardClient.ShardId == 0) { _updateTimer = new Timer((_) => { - HandleEvent(s => s.AddPeriodicBreadcrumb(), __ => UpdatePeriodic()); + HandleEvent(_ => UpdatePeriodic()); }, null, TimeSpan.Zero, TimeSpan.FromMinutes(1)); - Console.WriteLine( - $"PluralKit started as {_client.CurrentUser.Username}#{_client.CurrentUser.Discriminator} ({_client.CurrentUser.Id})."); - } + _logger.Information("PluralKit started as {Username}#{Discriminator} ({Id})", _client.CurrentUser.Username, _client.CurrentUser.Discriminator, _client.CurrentUser.Id); + } return Task.CompletedTask; } - private Task HandleEvent(Action breadcrumbFactory, Func handler) + private Task HandleEvent(Func handler) { + _logger.Debug("Received event"); + // Inner function so we can await the handler without stalling the entire pipeline async Task Inner() { @@ -243,46 +203,36 @@ namespace PluralKit.Bot // This prevents any synchronous nonsense from also stalling the pipeline before the first await point await Task.Yield(); - // Create a DI scope for this event - // and log the breadcrumb to the newly created (in-svc-scope) Sentry scope - using (var scope = _services.CreateScope()) - { - var evtid = scope.ServiceProvider.GetService().EventId; - - try - { - await handler(scope.ServiceProvider.GetRequiredService()); - } - catch (Exception e) - { - var sentryScope = scope.ServiceProvider.GetRequiredService(); - sentryScope.SetTag("evtid", evtid.ToString()); - breadcrumbFactory(sentryScope); - - HandleRuntimeError(e, scope.ServiceProvider); - } - } + using var containerScope = _services.BeginLifetimeScope(); + var sentryScope = containerScope.Resolve(); + var eventHandler = containerScope.Resolve(); + try + { + await handler(eventHandler); + } + catch (Exception e) + { + await HandleRuntimeError(eventHandler, e, sentryScope); + } } -#pragma warning disable 4014 - Inner(); -#pragma warning restore 4014 + var _ = Inner(); return Task.CompletedTask; } - private void HandleRuntimeError(Exception e, IServiceProvider services) + private async Task HandleRuntimeError(PKEventHandler eventHandler, Exception exc, Scope scope) { - var logger = services.GetRequiredService(); - var scope = services.GetRequiredService(); + _logger.Error(exc, "Exception in bot event handler"); - logger.Error(e, "Exception in bot event handler"); - - var evt = new SentryEvent(e); + var evt = new SentryEvent(exc); // Don't blow out our Sentry budget on sporadic not-our-problem erorrs - if (e.IsOurProblem()) + if (exc.IsOurProblem()) SentrySdk.CaptureEvent(evt, scope); + + // Once we've sent it to Sentry, report it to the user + await eventHandler.ReportError(evt, exc); } } @@ -292,28 +242,33 @@ namespace PluralKit.Bot private IMetrics _metrics; private DiscordShardedClient _client; private DbConnectionFactory _connectionFactory; - private IServiceProvider _services; + private ILifetimeScope _services; private CommandTree _tree; - private IDataStore _data; + private Scope _sentryScope; - public PKEventHandler(ProxyService proxy, ILogger logger, IMetrics metrics, IDiscordClient client, DbConnectionFactory connectionFactory, IServiceProvider services, CommandTree tree, IDataStore data) + // We're defining in the Autofac module that this class is instantiated with one instance per event + // This means that the HandleMessage function will either be called once, or not at all + // The ReportError function will be called on an error, and needs to refer back to the "trigger message" + // hence, we just store it in a local variable, ignoring it entirely if it's null. + private IUserMessage _msg = null; + + public PKEventHandler(ProxyService proxy, ILogger logger, IMetrics metrics, DiscordShardedClient client, DbConnectionFactory connectionFactory, ILifetimeScope services, CommandTree tree, Scope sentryScope) { _proxy = proxy; _logger = logger; _metrics = metrics; - _client = (DiscordShardedClient) client; + _client = client; _connectionFactory = connectionFactory; _services = services; _tree = tree; - _data = data; + _sentryScope = sentryScope; } public async Task HandleMessage(SocketMessage arg) { if (_client.GetShardFor((arg.Channel as IGuildChannel)?.Guild).ConnectionState != ConnectionState.Connected) return; // Discard messages while the bot "catches up" to avoid unnecessary CPU pressure causing timeouts - - + RegisterMessageMetrics(arg); // Ignore system messages (member joined, message pinned, etc) @@ -323,6 +278,16 @@ namespace PluralKit.Bot // Ignore bot messages if (msg.Author.IsBot || msg.Author.IsWebhook) return; + // Add message info as Sentry breadcrumb + _msg = msg; + _sentryScope.AddBreadcrumb(msg.Content, "event.message", data: new Dictionary + { + {"user", msg.Author.Id.ToString()}, + {"channel", msg.Channel.Id.ToString()}, + {"guild", ((msg.Channel as IGuildChannel)?.GuildId ?? 0).ToString()}, + {"message", msg.Id.ToString()}, + }); + int argPos = -1; // Check if message starts with the command prefix if (msg.Content.StartsWith("pk;", StringComparison.InvariantCultureIgnoreCase)) argPos = 3; @@ -349,18 +314,8 @@ namespace PluralKit.Bot system = await conn.QueryFirstOrDefaultAsync( "select systems.* from systems, accounts where accounts.uid = @Id and systems.id = accounts.system", new {Id = msg.Author.Id}); - - try - { - await _tree.ExecuteCommand(new Context(_services, msg, argPos, system)); - } - catch (Exception e) - { - await HandleCommandError(msg, e); - // HandleCommandError only *reports* the error, we gotta pass it through to the parent - // error handler by rethrowing: - throw; - } + + await _tree.ExecuteCommand(new Context(_services, msg, argPos, system)); } else { @@ -376,16 +331,19 @@ namespace PluralKit.Bot } } - private async Task HandleCommandError(SocketUserMessage msg, Exception exception) + public async Task ReportError(SentryEvent evt, Exception exc) { + // If we don't have a "trigger message", bail + if (_msg == null) return; + // This function *specifically* handles reporting a command execution error to the user. // We'll fetch the event ID and send a user-facing error message. // ONLY IF this error's actually our problem. As for what defines an error as "our problem", // check the extension method :) - if (exception.IsOurProblem()) + if (exc.IsOurProblem()) { - var eid = _services.GetService().EventId; - await msg.Channel.SendMessageAsync( + var eid = evt.EventId; + await _msg.Channel.SendMessageAsync( $"{Emojis.Error} Internal error occurred. Please join the support server (), and send the developer this ID: `{eid}`\nBe sure to include a description of what you were doing to make the error occur."); } @@ -401,12 +359,43 @@ namespace PluralKit.Bot } public Task HandleReactionAdded(Cacheable message, ISocketMessageChannel channel, - SocketReaction reaction) => _proxy.HandleReactionAddedAsync(message, channel, reaction); + SocketReaction reaction) + { + _sentryScope.AddBreadcrumb("", "event.reaction", data: new Dictionary() + { + {"user", reaction.UserId.ToString()}, + {"channel", channel.Id.ToString()}, + {"guild", ((channel as IGuildChannel)?.GuildId ?? 0).ToString()}, + {"message", message.Id.ToString()}, + {"reaction", reaction.Emote.Name} + }); + + return _proxy.HandleReactionAddedAsync(message, channel, reaction); + } - public Task HandleMessageDeleted(Cacheable message, ISocketMessageChannel channel) => - _proxy.HandleMessageDeletedAsync(message, channel); + public Task HandleMessageDeleted(Cacheable message, ISocketMessageChannel channel) + { + _sentryScope.AddBreadcrumb("", "event.messageDelete", data: new Dictionary() + { + {"channel", channel.Id.ToString()}, + {"guild", ((channel as IGuildChannel)?.GuildId ?? 0).ToString()}, + {"message", message.Id.ToString()}, + }); + + return _proxy.HandleMessageDeletedAsync(message, channel); + } public Task HandleMessagesBulkDelete(IReadOnlyCollection> messages, - IMessageChannel channel) => _proxy.HandleMessageBulkDeleteAsync(messages, channel); + IMessageChannel channel) + { + _sentryScope.AddBreadcrumb("", "event.messageDelete", data: new Dictionary() + { + {"channel", channel.Id.ToString()}, + {"guild", ((channel as IGuildChannel)?.GuildId ?? 0).ToString()}, + {"messages", string.Join(",", messages.Select(m => m.Id))}, + }); + + return _proxy.HandleMessageBulkDeleteAsync(messages, channel); + } } } diff --git a/PluralKit.Bot/BreadcrumbExtensions.cs b/PluralKit.Bot/BreadcrumbExtensions.cs deleted file mode 100644 index 4a002d3e..00000000 --- a/PluralKit.Bot/BreadcrumbExtensions.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Discord; -using Discord.WebSocket; -using Sentry; - -namespace PluralKit.Bot -{ - public static class BreadcrumbExtensions - { - public static void AddMessageBreadcrumb(this Scope scope, SocketMessage msg) - { - scope.AddBreadcrumb(msg.Content, "event.message", data: new Dictionary() - { - {"user", msg.Author.Id.ToString()}, - {"channel", msg.Channel.Id.ToString()}, - {"guild", ((msg.Channel as IGuildChannel)?.GuildId ?? 0).ToString()}, - {"message", msg.Id.ToString()}, - }); - } - - public static void AddReactionAddedBreadcrumb(this Scope scope, Cacheable message, - ISocketMessageChannel channel, SocketReaction reaction) - { - scope.AddBreadcrumb("", "event.reaction", data: new Dictionary() - { - {"user", reaction.UserId.ToString()}, - {"channel", channel.Id.ToString()}, - {"guild", ((channel as IGuildChannel)?.GuildId ?? 0).ToString()}, - {"message", message.Id.ToString()}, - {"reaction", reaction.Emote.Name} - }); - } - - public static void AddMessageDeleteBreadcrumb(this Scope scope, Cacheable message, - ISocketMessageChannel channel) - { - scope.AddBreadcrumb("", "event.messageDelete", data: new Dictionary() - { - {"channel", channel.Id.ToString()}, - {"guild", ((channel as IGuildChannel)?.GuildId ?? 0).ToString()}, - {"message", message.Id.ToString()}, - }); - } - - public static void AddMessageBulkDeleteBreadcrumb(this Scope scope, IReadOnlyCollection> messages, - ISocketMessageChannel channel) - { - scope.AddBreadcrumb("", "event.messageDelete", data: new Dictionary() - { - {"channel", channel.Id.ToString()}, - {"guild", ((channel as IGuildChannel)?.GuildId ?? 0).ToString()}, - {"messages", string.Join(",", messages.Select(m => m.Id))}, - }); - } - - public static void AddPeriodicBreadcrumb(this Scope scope) => scope.AddBreadcrumb("", "periodic"); - } -} \ No newline at end of file diff --git a/PluralKit.Bot/CommandSystem/Context.cs b/PluralKit.Bot/CommandSystem/Context.cs index 0f08edf1..0469deba 100644 --- a/PluralKit.Bot/CommandSystem/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context.cs @@ -3,6 +3,9 @@ using System.Threading.Tasks; using App.Metrics; +using Autofac; +using Autofac.Core; + using Discord; using Discord.WebSocket; @@ -12,7 +15,7 @@ namespace PluralKit.Bot.CommandSystem { public class Context { - private IServiceProvider _provider; + private ILifetimeScope _provider; private readonly DiscordShardedClient _client; private readonly SocketUserMessage _message; @@ -24,14 +27,14 @@ namespace PluralKit.Bot.CommandSystem private Command _currentCommand; - public Context(IServiceProvider provider, SocketUserMessage message, int commandParseOffset, + public Context(ILifetimeScope provider, SocketUserMessage message, int commandParseOffset, PKSystem senderSystem) { - _client = provider.GetRequiredService() as DiscordShardedClient; + _client = provider.Resolve(); _message = message; - _data = provider.GetRequiredService(); + _data = provider.Resolve(); _senderSystem = senderSystem; - _metrics = provider.GetRequiredService(); + _metrics = provider.Resolve(); _provider = provider; _parameters = new Parameters(message.Content.Substring(commandParseOffset)); } @@ -86,7 +89,7 @@ namespace PluralKit.Bot.CommandSystem try { - await handler(_provider.GetRequiredService()); + await handler(_provider.Resolve()); _metrics.Measure.Meter.Mark(BotMetrics.CommandsRun); } catch (PKSyntaxError e) diff --git a/PluralKit.Bot/Modules.cs b/PluralKit.Bot/Modules.cs new file mode 100644 index 00000000..62ce1eb8 --- /dev/null +++ b/PluralKit.Bot/Modules.cs @@ -0,0 +1,80 @@ +using System; +using System.Net.Http; + +using Autofac; +using Autofac.Extensions.DependencyInjection; + +using Discord; +using Discord.Rest; +using Discord.WebSocket; + +using Microsoft.Extensions.DependencyInjection; + +using PluralKit.Bot.Commands; + +using Sentry; + +namespace PluralKit.Bot +{ + public class BotModule: Module + { + protected override void Load(ContainerBuilder builder) + { + // Client + builder.Register(c => new DiscordShardedClient(new DiscordSocketConfig() + { + MessageCacheSize = 0, + ConnectionTimeout = 2 * 60 * 1000, + ExclusiveBulkDelete = true, + LargeThreshold = 50, + DefaultRetryMode = RetryMode.RetryTimeouts | RetryMode.RetryRatelimit + // Commented this out since Debug actually sends, uh, quite a lot that's not necessary in production + // but leaving it here in case I (or someone else) get[s] confused about why logging isn't working again :p + // LogLevel = LogSeverity.Debug // We filter log levels in Serilog, so just pass everything through (Debug is lower than Verbose) + })).AsSelf().As().As().As().SingleInstance(); + + // Commands + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + builder.RegisterType().AsSelf(); + + // Bot core + builder.RegisterType().AsSelf().SingleInstance(); + builder.RegisterType().AsSelf(); + + // Bot services + builder.RegisterType().AsSelf().SingleInstance(); + builder.RegisterType().AsSelf().SingleInstance(); + builder.RegisterType().AsSelf().SingleInstance(); + builder.RegisterType().AsSelf().SingleInstance(); + builder.RegisterType().AsSelf().SingleInstance(); + builder.RegisterType().AsSelf().SingleInstance(); + builder.RegisterType().AsSelf().SingleInstance(); + builder.RegisterType().AsSelf().SingleInstance(); + builder.RegisterType().AsSelf().SingleInstance(); + builder.RegisterType().AsSelf().SingleInstance(); + builder.RegisterType().AsSelf().SingleInstance(); + + // Sentry stuff + builder.Register(_ => new Scope(null)).AsSelf().InstancePerLifetimeScope(); + + // .NET stuff + builder.Populate(new ServiceCollection() + .AddMemoryCache()); + + // Utils + builder.Register(c => new HttpClient + { + Timeout = TimeSpan.FromSeconds(5) + }).AsSelf().SingleInstance(); + } + } +} \ No newline at end of file diff --git a/PluralKit.Core/Modules.cs b/PluralKit.Core/Modules.cs new file mode 100644 index 00000000..c7a90a6e --- /dev/null +++ b/PluralKit.Core/Modules.cs @@ -0,0 +1,109 @@ +using System; + +using App.Metrics; + +using Autofac; + +using Microsoft.Extensions.Configuration; + +using NodaTime; + +using Serilog; +using Serilog.Events; +using Serilog.Formatting.Compact; +using Serilog.Sinks.SystemConsole.Themes; + +namespace PluralKit.Core +{ + public class DataStoreModule: Module + { + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType().SingleInstance(); + builder.RegisterType().AsSelf().SingleInstance(); + builder.RegisterType().AsSelf().As(); + builder.RegisterType().AsSelf(); + } + } + + public class ConfigModule: Module where T: new() + { + private string _submodule; + + public ConfigModule(string submodule = null) + { + _submodule = submodule; + } + + protected override void Load(ContainerBuilder builder) + { + // We're assuming IConfiguration is already available somehow - it comes from various places (auto-injected in ASP, etc) + + // Register the CoreConfig and where to find it + builder.Register(c => c.Resolve().GetSection("PluralKit").Get() ?? new CoreConfig()).SingleInstance(); + + // Register the submodule config (BotConfig, etc) if specified + if (_submodule != null) + builder.Register(c => c.Resolve().GetSection("PluralKit").GetSection(_submodule).Get() ?? new T()).SingleInstance(); + } + } + + public class MetricsModule: Module + { + private readonly string _onlyContext; + + public MetricsModule(string onlyContext = null) + { + _onlyContext = onlyContext; + } + + protected override void Load(ContainerBuilder builder) + { + builder.Register(c => InitMetrics(c.Resolve())) + .AsSelf().As(); + } + + private IMetricsRoot InitMetrics(CoreConfig config) + { + var builder = AppMetrics.CreateDefaultBuilder(); + if (config.InfluxUrl != null && config.InfluxDb != null) + builder.Report.ToInfluxDb(config.InfluxUrl, config.InfluxDb); + if (_onlyContext != null) + builder.Filter.ByIncludingOnlyContext(_onlyContext); + return builder.Build(); + } + } + + public class LoggingModule: Module + { + private readonly string _component; + + public LoggingModule(string component) + { + _component = component; + } + + protected override void Load(ContainerBuilder builder) + { + builder.Register(c => InitLogger(c.Resolve())).AsSelf().SingleInstance(); + } + + private ILogger InitLogger(CoreConfig config) + { + return new LoggerConfiguration() + .ConfigureForNodaTime(DateTimeZoneProviders.Tzdb) + .MinimumLevel.Debug() + .WriteTo.Async(a => + a.File( + new RenderedCompactJsonFormatter(), + (config.LogDir ?? "logs") + $"/pluralkit.{_component}.log", + rollingInterval: RollingInterval.Day, + flushToDiskInterval: TimeSpan.FromSeconds(10), + restrictedToMinimumLevel: LogEventLevel.Information, + buffered: true)) + .WriteTo.Async(a => + a.Console(theme: AnsiConsoleTheme.Code, outputTemplate:"[{Timestamp:HH:mm:ss}] {Level:u3} {Message:lj}{NewLine}{Exception}")) + .CreateLogger(); + } + } +} \ No newline at end of file diff --git a/PluralKit.Core/PluralKit.Core.csproj b/PluralKit.Core/PluralKit.Core.csproj index 7352dd2f..cb450314 100644 --- a/PluralKit.Core/PluralKit.Core.csproj +++ b/PluralKit.Core/PluralKit.Core.csproj @@ -7,6 +7,8 @@ + + diff --git a/PluralKit.Core/Utils.cs b/PluralKit.Core/Utils.cs index c448ff0b..17b02955 100644 --- a/PluralKit.Core/Utils.cs +++ b/PluralKit.Core/Utils.cs @@ -324,35 +324,7 @@ namespace PluralKit // Add global type mapper for ProxyTag compound type in Postgres NpgsqlConnection.GlobalTypeMapper.MapComposite("proxy_tag"); } - - public static ILogger InitLogger(CoreConfig config, string component) - { - return new LoggerConfiguration() - .ConfigureForNodaTime(DateTimeZoneProviders.Tzdb) - .MinimumLevel.Debug() - .WriteTo.Async(a => - a.File( - new RenderedCompactJsonFormatter(), - (config.LogDir ?? "logs") + $"/pluralkit.{component}.log", - rollingInterval: RollingInterval.Day, - flushToDiskInterval: TimeSpan.FromSeconds(10), - restrictedToMinimumLevel: LogEventLevel.Information, - buffered: true)) - .WriteTo.Async(a => - a.Console(theme: AnsiConsoleTheme.Code, outputTemplate:"[{Timestamp:HH:mm:ss}] [{EventId}] {Level:u3} {Message:lj}{NewLine}{Exception}")) - .CreateLogger(); - } - - public static IMetrics InitMetrics(CoreConfig config, string onlyContext = null) - { - var builder = AppMetrics.CreateDefaultBuilder(); - if (config.InfluxUrl != null && config.InfluxDb != null) - builder.Report.ToInfluxDb(config.InfluxUrl, config.InfluxDb); - if (onlyContext != null) - builder.Filter.ByIncludingOnlyContext(onlyContext); - return builder.Build(); - } - + public static JsonSerializerSettings BuildSerializerSettings() => new JsonSerializerSettings().BuildSerializerSettings(); public static JsonSerializerSettings BuildSerializerSettings(this JsonSerializerSettings settings) @@ -362,18 +334,6 @@ namespace PluralKit } } - public class LoggerProvider - { - private CoreConfig _config; - public ILogger RootLogger { get; } - - public LoggerProvider(CoreConfig config, string component) - { - _config = config; - RootLogger = InitUtils.InitLogger(_config, component); - } - } - public class UlongEncodeAsLongHandler : SqlMapper.TypeHandler { public override ulong Parse(object value) @@ -660,16 +620,6 @@ namespace PluralKit } } - public class EventIdProvider - { - public Guid EventId { get; } - - public EventIdProvider() - { - EventId = Guid.NewGuid(); - } - } - public static class ConnectionUtils { public static async IAsyncEnumerable QueryStreamAsync(this DbConnectionFactory connFactory, string sql, object param)