Simplified logging, added logging to file
This commit is contained in:
parent
8d081ed637
commit
e1a39f964f
7 changed files with 74 additions and 82 deletions
|
@ -4,8 +4,6 @@ using System.Reflection;
|
||||||
namespace RegexBot;
|
namespace RegexBot;
|
||||||
|
|
||||||
static class ModuleLoader {
|
static class ModuleLoader {
|
||||||
private const string LogName = nameof(ModuleLoader);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Given the instance configuration, loads all appropriate types from file specified in it.
|
/// Given the instance configuration, loads all appropriate types from file specified in it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -42,13 +40,12 @@ static class ModuleLoader {
|
||||||
where !type.IsAssignableFrom(typeof(RegexbotModule))
|
where !type.IsAssignableFrom(typeof(RegexbotModule))
|
||||||
where type.GetCustomAttribute<RegexbotModuleAttribute>() != null
|
where type.GetCustomAttribute<RegexbotModuleAttribute>() != null
|
||||||
select type;
|
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>();
|
var newmods = new List<RegexbotModule>();
|
||||||
foreach (var t in eligibleTypes) {
|
foreach (var t in eligibleTypes) {
|
||||||
var mod = Activator.CreateInstance(t, k)!;
|
var mod = Activator.CreateInstance(t, k)!;
|
||||||
k._svcLogging.DoInstanceLog(false, LogName,
|
k._svcLogging.DoLog(false, nameof(ModuleLoader), $"---> Loading module {t.FullName}");
|
||||||
$"---> Loading module {t.FullName}");
|
|
||||||
newmods.Add((RegexbotModule)mod);
|
newmods.Add((RegexbotModule)mod);
|
||||||
}
|
}
|
||||||
return newmods;
|
return newmods;
|
||||||
|
|
|
@ -50,19 +50,16 @@ class Program {
|
||||||
private static void Console_CancelKeyPress(object? sender, ConsoleCancelEventArgs e) {
|
private static void Console_CancelKeyPress(object? sender, ConsoleCancelEventArgs e) {
|
||||||
e.Cancel = true;
|
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 finishingTasks = Task.Run(async () => {
|
||||||
var closeWait = Task.Delay(5000);
|
// TODO periodic task service: stop processing, wait for all tasks to finish
|
||||||
|
// TODO notify services of shutdown
|
||||||
|
await _main.DiscordClient.StopAsync();
|
||||||
|
});
|
||||||
|
|
||||||
// TODO periodic task service: stop processing, wait for all tasks to finish
|
if (!finishingTasks.Wait(5000))
|
||||||
// TODO notify services of shutdown
|
_main._svcLogging.DoLog(false, nameof(RegexBot), "Could not disconnect properly. Exiting...");
|
||||||
|
|
||||||
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.");
|
|
||||||
Environment.Exit(0);
|
Environment.Exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,6 @@ public partial class RegexbotClient {
|
||||||
|
|
||||||
// Everything's ready to go. Print the welcome message here.
|
// Everything's ready to go. Print the welcome message here.
|
||||||
var ver = Assembly.GetExecutingAssembly().GetName().Version!.ToString(3);
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,18 +69,21 @@ public abstract class RegexbotModule {
|
||||||
protected EntityList GetModerators(ulong guild) => Bot._svcGuildState.DoGetModlist(guild);
|
protected EntityList GetModerators(ulong guild) => Bot._svcGuildState.DoGetModlist(guild);
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// /// <param name="message">The log message to send. Multi-line messages are acceptable.</param>
|
/// /// <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>
|
/// <summary>
|
||||||
/// Sends a message to the instance log.
|
/// Emits a log message to the bot console and, optionally, the logging webhook.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">The log message to send. Multi-line messages are acceptable.</param>
|
/// <param name="message">The log message to send. Multi-line messages are acceptable.</param>
|
||||||
/// <param name="report">
|
/// <param name="report">
|
||||||
/// Specifies if the log message should be sent to the reporting channel.
|
/// Specifies if the log message should be sent to the reporting channel.
|
||||||
/// Only messages of very high importance should use this option.
|
/// Only messages of very high importance should use this option.
|
||||||
/// </param>
|
/// </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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,30 @@
|
||||||
using Discord;
|
using Discord;
|
||||||
using Discord.Webhook;
|
using Discord.Webhook;
|
||||||
using RegexBot.Data;
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace RegexBot.Services.Logging;
|
namespace RegexBot.Services.Logging;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implements logging. Logging is distinguished into two types: Instance and per-guild.
|
/// Implements program-wide logging.
|
||||||
/// For further information on log types, see documentation under <see cref="Data.BotDatabaseContext"/>.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class LoggingService : Service {
|
class LoggingService : Service {
|
||||||
// NOTE: Service.Log's functionality is implemented here. DO NOT use within this class.
|
// NOTE: Service.Log's functionality is implemented here. DO NOT use within this class.
|
||||||
private readonly DiscordWebhookClient _instLogWebhook;
|
private readonly DiscordWebhookClient _instLogWebhook;
|
||||||
|
private readonly string? _logBasePath;
|
||||||
|
|
||||||
internal LoggingService(RegexbotClient bot) : base(bot) {
|
internal LoggingService(RegexbotClient bot) : base(bot) {
|
||||||
_instLogWebhook = new DiscordWebhookClient(bot.Config.InstanceLogTarget);
|
_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;
|
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>
|
/// <summary>
|
||||||
|
@ -27,30 +32,45 @@ class LoggingService : Service {
|
||||||
/// Only events with high importance are stored. Others are just printed to console.
|
/// Only events with high importance are stored. Others are just printed to console.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private Task DiscordClient_Log(LogMessage arg) {
|
private Task DiscordClient_Log(LogMessage arg) {
|
||||||
bool important = arg.Severity != LogSeverity.Info;
|
var msg = $"[{Enum.GetName(typeof(LogSeverity), arg.Severity)}] {arg.Message}";
|
||||||
string msg = $"[{Enum.GetName(typeof(LogSeverity), arg.Severity)}] {arg.Message}";
|
|
||||||
const string logSource = "Discord.Net";
|
|
||||||
if (arg.Exception != null) msg += "\n```\n" + arg.Exception.ToString() + "\n```";
|
if (arg.Exception != null) msg += "\n```\n" + arg.Exception.ToString() + "\n```";
|
||||||
|
|
||||||
if (important) DoInstanceLog(true, logSource, msg);
|
var important = arg.Severity != LogSeverity.Info;
|
||||||
else ToConsole(logSource, msg);
|
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;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ToConsole(string source, string message) {
|
private void Output(string source, string message) {
|
||||||
message ??= "(null)";
|
var now = DateTimeOffset.UtcNow;
|
||||||
var prefix = $"[{DateTimeOffset.UtcNow:u}] [{source}] ";
|
var output = new StringBuilder();
|
||||||
|
var prefix = $"[{now:u}] [{source}] ";
|
||||||
foreach (var line in message.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None)) {
|
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
|
// Hooked
|
||||||
internal void DoInstanceLog(bool report, string source, string? message) {
|
internal void DoLog(bool report, string source, string? message) {
|
||||||
message ??= "(null)";
|
message ??= "(null)";
|
||||||
ToConsole(source, message);
|
Output(source, message);
|
||||||
|
|
||||||
if (report) Task.Run(() => ReportInstanceWebhook(source, message));
|
if (report) Task.Run(() => ReportInstanceWebhook(source, message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,24 +82,8 @@ class LoggingService : Service {
|
||||||
Description = message
|
Description = message
|
||||||
};
|
};
|
||||||
await _instLogWebhook.SendMessageAsync(embeds: new[] { e.Build() });
|
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) {
|
} catch (Exception ex) {
|
||||||
// Stack trace goes to console only.
|
DoLog(false, Name, "Failed to send message to reporting channel: " + ex.Message);
|
||||||
DoInstanceLog(false, Name, "Error when storing guild log line: " + ex.ToString());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,10 +27,7 @@ class ModuleStateService : Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RefreshGuildState(SocketGuild arg) {
|
private async Task RefreshGuildState(SocketGuild arg) {
|
||||||
bool success = await ProcessConfiguration(arg.Id);
|
if (await ProcessConfiguration(arg.Id)) Log($"Configuration refreshed for server {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.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task RemoveGuildData(SocketGuild arg) {
|
private Task RemoveGuildData(SocketGuild arg) {
|
||||||
|
@ -79,7 +76,7 @@ class ModuleStateService : Service {
|
||||||
throw new InvalidCastException("Configuration is not valid JSON.");
|
throw new InvalidCastException("Configuration is not valid JSON.");
|
||||||
}
|
}
|
||||||
} catch (Exception ex) when (ex is JsonReaderException or InvalidCastException) {
|
} 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,25 +87,19 @@ class ModuleStateService : Service {
|
||||||
|
|
||||||
// Create guild state objects for all existing modules
|
// Create guild state objects for all existing modules
|
||||||
var newStates = new Dictionary<Type, object?>();
|
var newStates = new Dictionary<Type, object?>();
|
||||||
foreach (var mod in BotClient.Modules) {
|
foreach (var module in BotClient.Modules) {
|
||||||
var t = mod.GetType();
|
var t = module.GetType();
|
||||||
var tn = t.Name;
|
|
||||||
try {
|
try {
|
||||||
try {
|
var state = await module.CreateGuildStateAsync(guildId, guildConf[module.Name]!);
|
||||||
var state = await mod.CreateGuildStateAsync(guildId, guildConf[tn]!);
|
newStates.Add(t, state);
|
||||||
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;
|
|
||||||
}
|
|
||||||
} catch (ModuleLoadException ex) {
|
} catch (ModuleLoadException ex) {
|
||||||
BotClient._svcLogging.DoGuildLog(guildId, GuildLogSource,
|
Log($"{guildId}: Error reading configuration regarding {module.Name}: {ex.Message}");
|
||||||
$"{tn} has encountered an issue with its configuration: {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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,5 +19,5 @@ internal abstract class Service {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">The log message to send. Multi-line messages are acceptable.</param>
|
/// <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>
|
/// <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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue