From e1a39f964f92157a8dadada00ff12e24a08cee79 Mon Sep 17 00:00:00 2001 From: Noi Date: Thu, 19 May 2022 17:03:03 -0700 Subject: [PATCH] Simplified logging, added logging to file --- RegexBot/ModuleLoader.cs | 7 +- RegexBot/Program.cs | 21 +++-- RegexBot/RegexbotClient.cs | 2 +- RegexBot/RegexbotModule.cs | 11 ++- RegexBot/Services/Logging/LoggingService.cs | 78 ++++++++++--------- .../ModuleState/ModuleStateService.cs | 35 ++++----- RegexBot/Services/Service.cs | 2 +- 7 files changed, 74 insertions(+), 82 deletions(-) diff --git a/RegexBot/ModuleLoader.cs b/RegexBot/ModuleLoader.cs index 6007b2b..43bda08 100644 --- a/RegexBot/ModuleLoader.cs +++ b/RegexBot/ModuleLoader.cs @@ -4,8 +4,6 @@ using System.Reflection; namespace RegexBot; static class ModuleLoader { - private const string LogName = nameof(ModuleLoader); - /// /// Given the instance configuration, loads all appropriate types from file specified in it. /// @@ -42,13 +40,12 @@ static class ModuleLoader { where !type.IsAssignableFrom(typeof(RegexbotModule)) where type.GetCustomAttribute() != null select type; - k._svcLogging.DoInstanceLog(false, LogName, $"Scanning {asm.GetName().Name}"); + k._svcLogging.DoLog(false, nameof(ModuleLoader), $"Scanning {asm.GetName().Name}"); var newmods = new List(); foreach (var t in eligibleTypes) { var mod = Activator.CreateInstance(t, k)!; - k._svcLogging.DoInstanceLog(false, LogName, - $"---> Loading module {t.FullName}"); + k._svcLogging.DoLog(false, nameof(ModuleLoader), $"---> Loading module {t.FullName}"); newmods.Add((RegexbotModule)mod); } return newmods; diff --git a/RegexBot/Program.cs b/RegexBot/Program.cs index 4ae93c7..e8d821d 100644 --- a/RegexBot/Program.cs +++ b/RegexBot/Program.cs @@ -50,19 +50,16 @@ class Program { private static void Console_CancelKeyPress(object? sender, ConsoleCancelEventArgs e) { e.Cancel = true; - _main._svcLogging.DoInstanceLog(true, nameof(RegexBot), "Shutting down. Reason: Interrupt signal."); + _main._svcLogging.DoLog(true, nameof(RegexBot), "Shutting down. Reason: Interrupt signal."); - // 5 seconds of leeway - any currently running tasks will need time to finish executing - var closeWait = Task.Delay(5000); - - // TODO periodic task service: stop processing, wait for all tasks to finish - // TODO notify services of shutdown - - closeWait.Wait(); - - bool success = _main.DiscordClient.StopAsync().Wait(1000); - if (!success) _main._svcLogging.DoInstanceLog(false, nameof(RegexBot), - "Failed to disconnect cleanly from Discord. Will force shut down."); + var finishingTasks = Task.Run(async () => { + // TODO periodic task service: stop processing, wait for all tasks to finish + // TODO notify services of shutdown + await _main.DiscordClient.StopAsync(); + }); + + if (!finishingTasks.Wait(5000)) + _main._svcLogging.DoLog(false, nameof(RegexBot), "Could not disconnect properly. Exiting..."); Environment.Exit(0); } } diff --git a/RegexBot/RegexbotClient.cs b/RegexBot/RegexbotClient.cs index f11c0b4..fc66b1f 100644 --- a/RegexBot/RegexbotClient.cs +++ b/RegexBot/RegexbotClient.cs @@ -37,6 +37,6 @@ public partial class RegexbotClient { // Everything's ready to go. Print the welcome message here. var ver = Assembly.GetExecutingAssembly().GetName().Version!.ToString(3); - _svcLogging.DoInstanceLog(false, nameof(RegexBot), $"{nameof(RegexBot)} v{ver}. https://github.com/NoiTheCat/RegexBot"); + _svcLogging.DoLog(true, nameof(RegexBot), $"{nameof(RegexBot)} v{ver} - https://github.com/NoiTheCat/RegexBot"); } } diff --git a/RegexBot/RegexbotModule.cs b/RegexBot/RegexbotModule.cs index 582af74..2a6fb16 100644 --- a/RegexBot/RegexbotModule.cs +++ b/RegexBot/RegexbotModule.cs @@ -69,18 +69,21 @@ public abstract class RegexbotModule { protected EntityList GetModerators(ulong guild) => Bot._svcGuildState.DoGetModlist(guild); /// - /// Appends a message to the specified guild log. + /// Emits a log message to the bot console that is associated with the specified guild. /// /// /// The log message to send. Multi-line messages are acceptable. - protected void Log(ulong guild, string message) => Bot._svcLogging.DoGuildLog(guild, Name, message); + protected void Log(SocketGuild guild, string? message) { + var gname = guild.Name ?? $"Guild ID {guild.Id}"; + Bot._svcLogging.DoLog(false, $"{Name}] [{gname}", message); + } /// - /// Sends a message to the instance log. + /// Emits a log message to the bot console and, optionally, the logging webhook. /// /// The log message to send. Multi-line messages are acceptable. /// /// Specifies if the log message should be sent to the reporting channel. /// Only messages of very high importance should use this option. /// - protected void PLog(string message, bool report = false) => Bot._svcLogging.DoInstanceLog(report, Name, message); + protected void Log(string message, bool report = false) => Bot._svcLogging.DoLog(report, Name, message); } diff --git a/RegexBot/Services/Logging/LoggingService.cs b/RegexBot/Services/Logging/LoggingService.cs index acfab82..2e02e97 100644 --- a/RegexBot/Services/Logging/LoggingService.cs +++ b/RegexBot/Services/Logging/LoggingService.cs @@ -1,25 +1,30 @@ using Discord; using Discord.Webhook; -using RegexBot.Data; +using System.Reflection; +using System.Text; namespace RegexBot.Services.Logging; - /// -/// Implements logging. Logging is distinguished into two types: Instance and per-guild. -/// For further information on log types, see documentation under . +/// Implements program-wide logging. /// class LoggingService : Service { // NOTE: Service.Log's functionality is implemented here. DO NOT use within this class. private readonly DiscordWebhookClient _instLogWebhook; + private readonly string? _logBasePath; internal LoggingService(RegexbotClient bot) : base(bot) { _instLogWebhook = new DiscordWebhookClient(bot.Config.InstanceLogTarget); + _logBasePath = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location) + + Path.DirectorySeparatorChar + "logs"; + try { + if (!Directory.Exists(_logBasePath)) Directory.CreateDirectory(_logBasePath); + Directory.GetFiles(_logBasePath); + } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { + _logBasePath = null; + Output(Name, "Cannot create or access logging directory. File logging will be disabled."); + } - // Discord.Net log handling (client logging option is specified in Program.cs) bot.DiscordClient.Log += DiscordClient_Log; - // Let's also do the ready message - bot.DiscordClient.Ready += - delegate { DoInstanceLog(true, nameof(RegexBot), "Connected and ready."); return Task.CompletedTask; }; } /// @@ -27,30 +32,45 @@ class LoggingService : Service { /// Only events with high importance are stored. Others are just printed to console. /// private Task DiscordClient_Log(LogMessage arg) { - bool important = arg.Severity != LogSeverity.Info; - string msg = $"[{Enum.GetName(typeof(LogSeverity), arg.Severity)}] {arg.Message}"; - const string logSource = "Discord.Net"; + var msg = $"[{Enum.GetName(typeof(LogSeverity), arg.Severity)}] {arg.Message}"; if (arg.Exception != null) msg += "\n```\n" + arg.Exception.ToString() + "\n```"; - if (important) DoInstanceLog(true, logSource, msg); - else ToConsole(logSource, msg); + var important = arg.Severity != LogSeverity.Info; + switch (arg.Message) { // Prevent webhook logs for these 'important' Discord.Net messages + case "Connecting": + case "Connected": + case "Ready": + case "Disconnecting": + case "Disconnected": + case "Resumed previous session": + case "Failed to resume previous session": + important = false; + break; + } + DoLog(important, "Discord.Net", msg); return Task.CompletedTask; } - private static void ToConsole(string source, string message) { - message ??= "(null)"; - var prefix = $"[{DateTimeOffset.UtcNow:u}] [{source}] "; + private void Output(string source, string message) { + var now = DateTimeOffset.UtcNow; + var output = new StringBuilder(); + var prefix = $"[{now:u}] [{source}] "; foreach (var line in message.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None)) { - Console.WriteLine(prefix + line); + output.Append(prefix).AppendLine(line); + } + var outstr = output.ToString(); + Console.Write(outstr); + if (_logBasePath != null) { + var filename = _logBasePath + Path.DirectorySeparatorChar + $"{now:yyyy-MM}.log"; + File.AppendAllText(filename, outstr, Encoding.UTF8); } } // Hooked - internal void DoInstanceLog(bool report, string source, string? message) { + internal void DoLog(bool report, string source, string? message) { message ??= "(null)"; - ToConsole(source, message); - + Output(source, message); if (report) Task.Run(() => ReportInstanceWebhook(source, message)); } @@ -62,24 +82,8 @@ class LoggingService : Service { Description = message }; await _instLogWebhook.SendMessageAsync(embeds: new[] { e.Build() }); - } catch (Discord.Net.HttpException ex) { - DoInstanceLog(false, Name, "Failed to send message to reporting channel: " + ex.Message); - } - } - - // Hooked - public void DoGuildLog(ulong guild, string source, string message) { - message ??= "(null)"; - try { - using var db = new BotDatabaseContext(); - db.Add(new GuildLogLine() { GuildId = (long)guild, Source = source, Message = message }); - db.SaveChanges(); -#if DEBUG - ToConsole($"DEBUG {guild} - {source}", message); -#endif } catch (Exception ex) { - // Stack trace goes to console only. - DoInstanceLog(false, Name, "Error when storing guild log line: " + ex.ToString()); + DoLog(false, Name, "Failed to send message to reporting channel: " + ex.Message); } } } diff --git a/RegexBot/Services/ModuleState/ModuleStateService.cs b/RegexBot/Services/ModuleState/ModuleStateService.cs index 0ccf3ed..d6aa513 100644 --- a/RegexBot/Services/ModuleState/ModuleStateService.cs +++ b/RegexBot/Services/ModuleState/ModuleStateService.cs @@ -27,10 +27,7 @@ class ModuleStateService : Service { } private async Task RefreshGuildState(SocketGuild arg) { - bool success = await ProcessConfiguration(arg.Id); - - if (success) BotClient._svcLogging.DoInstanceLog(false, GuildLogSource, $"Configuration refreshed for guild ID {arg.Id}."); - else BotClient._svcLogging.DoGuildLog(arg.Id, GuildLogSource, "Configuration was not refreshed due to errors."); + if (await ProcessConfiguration(arg.Id)) Log($"Configuration refreshed for server {arg.Id}."); } private Task RemoveGuildData(SocketGuild arg) { @@ -79,7 +76,7 @@ class ModuleStateService : Service { throw new InvalidCastException("Configuration is not valid JSON."); } } catch (Exception ex) when (ex is JsonReaderException or InvalidCastException) { - BotClient._svcLogging.DoGuildLog(guildId, GuildLogSource, $"A problem exists within the guild configuration: {ex.Message}"); + Log($"Error loading configuration for server ID {guildId}: {ex.Message}"); return false; } @@ -90,25 +87,19 @@ class ModuleStateService : Service { // Create guild state objects for all existing modules var newStates = new Dictionary(); - foreach (var mod in BotClient.Modules) { - var t = mod.GetType(); - var tn = t.Name; + foreach (var module in BotClient.Modules) { + var t = module.GetType(); try { - try { - var state = await mod.CreateGuildStateAsync(guildId, guildConf[tn]!); - newStates.Add(t, state); - } catch (Exception ex) when (ex is not ModuleLoadException) { - Log("Unhandled exception while initializing guild state for module:\n" + - $"Module: {tn} | " + - $"Guild: {guildId} ({BotClient.DiscordClient.GetGuild(guildId)?.Name ?? "unknown name"})\n" + - $"```\n{ex}\n```", true); - BotClient._svcLogging.DoGuildLog(guildId, GuildLogSource, - "An internal error occurred when attempting to load new configuration."); - return false; - } + var state = await module.CreateGuildStateAsync(guildId, guildConf[module.Name]!); + newStates.Add(t, state); } catch (ModuleLoadException ex) { - BotClient._svcLogging.DoGuildLog(guildId, GuildLogSource, - $"{tn} has encountered an issue with its configuration: {ex.Message}"); + Log($"{guildId}: Error reading configuration regarding {module.Name}: {ex.Message}"); + return false; + } catch (Exception ex) when (ex is not ModuleLoadException) { + Log("Unhandled exception while initializing guild state for module:\n" + + $"Module: {module.Name} | " + + $"Guild: {guildId} ({BotClient.DiscordClient.GetGuild(guildId)?.Name ?? "unknown name"})\n" + + $"```\n{ex}\n```", true); return false; } } diff --git a/RegexBot/Services/Service.cs b/RegexBot/Services/Service.cs index 6aa2b91..bdec16b 100644 --- a/RegexBot/Services/Service.cs +++ b/RegexBot/Services/Service.cs @@ -19,5 +19,5 @@ internal abstract class Service { /// /// The log message to send. Multi-line messages are acceptable. /// Specify if the log message should be sent to a reporting channel. - protected void Log(string message, bool report = false) => BotClient._svcLogging.DoInstanceLog(report, Name, message); + protected void Log(string message, bool report = false) => BotClient._svcLogging.DoLog(report, Name, message); }