Simplified logging, added logging to file

This commit is contained in:
Noi 2022-05-19 17:03:03 -07:00
parent 8d081ed637
commit e1a39f964f
7 changed files with 74 additions and 82 deletions

View file

@ -4,8 +4,6 @@ using System.Reflection;
namespace RegexBot;
static class ModuleLoader {
private const string LogName = nameof(ModuleLoader);
/// <summary>
/// Given the instance configuration, loads all appropriate types from file specified in it.
/// </summary>
@ -42,13 +40,12 @@ static class ModuleLoader {
where !type.IsAssignableFrom(typeof(RegexbotModule))
where type.GetCustomAttribute<RegexbotModuleAttribute>() != 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<RegexbotModule>();
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;

View file

@ -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);
}
}

View file

@ -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");
}
}

View file

@ -69,18 +69,21 @@ public abstract class RegexbotModule {
protected EntityList GetModerators(ulong guild) => Bot._svcGuildState.DoGetModlist(guild);
/// <summary>
/// Appends a message to the specified guild log.
/// Emits a log message to the bot console that is associated with the specified guild.
/// </summary>
/// /// <param name="message">The log message to send. Multi-line messages are acceptable.</param>
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);
}
/// <summary>
/// Sends a message to the instance log.
/// Emits a log message to the bot console and, optionally, the logging webhook.
/// </summary>
/// <param name="message">The log message to send. Multi-line messages are acceptable.</param>
/// <param name="report">
/// Specifies if the log message should be sent to the reporting channel.
/// Only messages of very high importance should use this option.
/// </param>
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);
}

View file

@ -1,25 +1,30 @@
using Discord;
using Discord.Webhook;
using RegexBot.Data;
using System.Reflection;
using System.Text;
namespace RegexBot.Services.Logging;
/// <summary>
/// Implements logging. Logging is distinguished into two types: Instance and per-guild.
/// For further information on log types, see documentation under <see cref="Data.BotDatabaseContext"/>.
/// Implements program-wide logging.
/// </summary>
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; };
}
/// <summary>
@ -27,30 +32,45 @@ class LoggingService : Service {
/// Only events with high importance are stored. Others are just printed to console.
/// </summary>
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);
}
}
}

View file

@ -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<Type, object?>();
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;
}
}

View file

@ -19,5 +19,5 @@ internal abstract class Service {
/// </summary>
/// <param name="message">The log message to send. Multi-line messages are acceptable.</param>
/// <param name="report">Specify if the log message should be sent to a reporting channel.</param>
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);
}